Your browser lacks required capabilities. Please upgrade it or switch to another to continue.
Loading…
{
"name": "Vesper",
"ifid": "7032a282-b49c-5de1-9ef0-8553b88d1f63",
"format": "SugarCube",
"format-version": "2.36.1",
"start": "Start"
}<<set $player = {
"name": "Wren",
"portrait": "wren.jpg",
"current_location": "",
"core_traits": {"energy": 100, "money": 50, "fighting": 0, "stealth": 0, "yard_depth": 0, "weapon_repair": 0, "coin": 0, "equipped_weapon": 1, "drain_charge": 1, "arousal_charge": 0, "corruption": 0, "arousal": 0, "hygiene": 100, "sex_stage": 0, "loop_npc_pleasure": 0, "sex_finisher_type": 0, "anal_active": 0, "sex_entry_origin": 0, "drains_done": 0, "names_known": 0, "core_strain": 0, "cell_door": 0, "core_sealed": 0, "kess_debt": 0, "repair_armed": 0, "repair_session": 0, "calloway_drains_done": 0, "colm_drains_done": 0, "case_progress": 0, "vault_cleared": 0, "emitter_broken": 0},
"wardrobe": {"company_grays": {"id": "company_grays", "name": "Company grays", "slot": "dress", "image": "./videos/clothing/company_grays.jpg"}, "plain_bra": {"id": "plain_bra", "name": "Plain bra", "slot": "bra", "image": "./videos/clothing/plain_bra.jpg"}, "plain_briefs": {"id": "plain_briefs", "name": "Plain briefs", "slot": "underwear", "image": "./videos/clothing/plain_briefs.jpg"}, "flat_shoes": {"id": "flat_shoes", "name": "Flats", "slot": "shoes", "image": "./videos/clothing/flat_shoes.jpg"}},
"equipped": {"bra": "plain_bra", "underwear": "plain_briefs", "top": null, "bottom": null, "dress": "company_grays", "legwear": null, "shoes": "flat_shoes"}
}>><<set $npcs = {"npc_mercer": {"name": "Mercer", "portrait": "mercer.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_renner": {"name": "Renner", "portrait": "renner.jpg", "core_traits": {"relation": 0, "corruption": 0, "arousal": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_sol": {"name": "Sol", "portrait": "sol.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_bastien": {"name": "Bastien", "portrait": "bastien.jpg", "core_traits": {"relation": 0, "corruption": 0, "arousal": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_rue": {"name": "Rue", "portrait": "rue.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_marsh": {"name": "Marsh", "portrait": "marsh.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_kess": {"name": "Kess", "portrait": "kess.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_calloway": {"name": "Calloway", "portrait": "calloway.jpg", "core_traits": {"relation": 0, "corruption": 0, "arousal": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_vane": {"name": "Vane", "portrait": "vane.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_colm": {"name": "Colm", "portrait": "colm.jpg", "core_traits": {"relation": 0, "corruption": 0, "arousal": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_enns": {"name": "Enns", "portrait": "enns.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_voss": {"name": "Voss", "portrait": "voss.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_reyes": {"name": "Reyes", "portrait": "reyes.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_marr": {"name": "Marr", "portrait": "marr.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}}>><<set $npc_interacted_today = {}>><<set $flags = {"game_started": true, "debug_mode": false}>><<set $flags_meta = {}>><<set $game_state = {
"current_canvas": "",
"visited_locations": [],
"visited_nodes": [],
"trigger_history": {},
"activity_trigger_history": {},
"visited_choices": {},
"active_modifiers": {},
"random_cooldowns": {},
"stage_advancement_log": {},
"quests": {},
"scheduled": [],
"fast_jobs": { "xp": 0, "cooldowns": {} },
"bank": { "balance": 0 },
"time_state": {
"current_hour": 20,
"current_minute": 0,
"current_day": "Monday",
"current_week": 1,
"day": 1
}
}>><<nobr>>
<div class="game-intro">
<h1>Vesper</h1>
<p class="game-description">An owned half-human weapon who feels nothing but the sex she's used for. Inside Vance Dynamics she's the company's slave; outside, under a false face, she slips into powerful men's lives and drains them while they think they're using her — sent to hunt a rogue called Cain.</p>
<div class="developer-intro">
<p class="developer-about">We're a small indie studio crafting intimate, story-driven experiences. Every game is made with care, and your support helps us keep creating. If you enjoy our work, consider supporting us!</p>
<p class="support-link">👉 <a href="https://www.patreon.com/cw/nutgames844" target="_blank" rel="noopener">Support us on Patreon</a></p>
</div>
<div class="age-gate">
<p class="age-warning">⚠️ This game contains adult content intended for players 18 years of age or older.</p>
<div class="age-buttons">
[[✓ I am 18 or older - Enter Game->StartingCanvas_canvas_opening_office_Node_office]]
[[✗ I am NOT 18 or older->AgeBlocked]]
</div>
</div>
<div class="developer-footer">
<p class="developer-credit">Developed by <strong>NutGames</strong></p>
<p class="support-link">👉 <a href="https://www.patreon.com/cw/nutgames844" target="_blank" rel="noopener">Support us on Patreon</a></p>
</div>
</div>
<</nobr>><div class="blocked-page">
<h2>Access Denied</h2>
<p>This content is not available for you.</p>
<p>Please close this page.</p>
</div><!-- Time Display Widgets -->
<<widget "timeFormatted">>
<<set _hour to $game_state.time_state.current_hour>>
<<set _minute to $game_state.time_state.current_minute>>
<<set _displayHour to (_hour is 0 ? 12 : (_hour > 12 ? _hour - 12 : _hour))>>
<<set _ampm to (_hour < 12 ? "AM" : "PM")>>
<<set _minuteStr to (_minute < 10 ? "0" + _minute : _minute)>>
<<print _displayHour + ":" + _minuteStr + " " + _ampm>>
<</widget>>
<!-- Render a quest card: narrative line + (Pattern 2) auto-rendered 🎯 goal
block + 💡 tip line. Author tags state-pressure variants in the TOML and
the picker swaps which line fires; widget renders the chosen line, then
calls setup.computeHintGoal() to render the structured progress block from
the hint's stage condition (or canvas trigger conditions for branch-inside-
shell transitions). Game-level mechanics (decay rates, time costs, etc.)
live on :: TipsPage instead. -->
<<widget "renderStageHint">>
/* Pattern 2 (2026-05-01): accepts either a string (legacy callers) OR a
hint object {text, condition, tip, auto_goal, ...}. When passed an object
with auto_goal=true and a stage condition, the engine computes the
structured 🎯 goal block (bullets + live progress + 📍 location). When
passed a string OR a no-auto-goal object, falls back to splitting on
" — 🎯 " (legacy behavior) so existing games keep working unchanged. */
<<set _hintArg to $args[0]>>
<<set _hintObj to (typeof _hintArg === "object" && _hintArg !== null) ? _hintArg : null>>
<<set _hintText to _hintObj ? (_hintObj.text || "") : (_hintArg || "")>>
<<set _computedGoalHtml to _hintObj ? setup.computeHintGoal(_hintObj) : "">>
<<set _separator to " — 🎯 ">>
<<set _splitIdx to _hintText.indexOf(_separator)>>
<<if _computedGoalHtml>>
/* New path: structured auto-render. If text contains " — 🎯 ", strip the
manual goal portion (auto-render replaces it). */
<<if _splitIdx gt -1>>
<<set _flavor to _hintText.substring(0, _splitIdx)>>
<<else>>
<<set _flavor to _hintText>>
<</if>>
/* 2026-05-09: when State C "🔓 Ready" fires AND the hint defines
ready_text, swap flavor → ready_text. The author-supplied ready_text
replaces the regular text once all helper gates are met. */
<<if _hintObj && _hintObj.ready_text && setup._isHintReady(_hintObj)>>
<<set _flavor to _hintObj.ready_text>>
<</if>>
<div class="stage-hint-card">
<div class="stage-hint-flavor"><<print _flavor>></div>
<<print _computedGoalHtml>>
<<if _hintObj && _hintObj.tip>>
<div class="stage-hint-tip">💡 <<print _hintObj.tip>></div>
<</if>>
</div>
<<elseif _splitIdx gt -1>>
/* Legacy path: split on " — 🎯 " */
<<set _flavor to _hintText.substring(0, _splitIdx)>>
<<set _goal to _hintText.substring(_splitIdx + _separator.length)>>
<div class="stage-hint-card">
<div class="stage-hint-flavor"><<print _flavor>></div>
<div class="stage-hint-goal"><span class="stage-hint-target">🎯</span> <<print _goal>></div>
<<if _hintObj && _hintObj.tip>>
<div class="stage-hint-tip">💡 <<print _hintObj.tip>></div>
<</if>>
</div>
<<elseif _hintText>>
<div class="stage-hint-card">
<div class="stage-hint-flavor"><<print _hintText>></div>
</div>
<</if>>
<</widget>>
<<widget "timeDisplay">>
<div id="time-widget" class="time-display">
<div class="time-line">
<span id="time-display"><<timeFormatted>></span> | <span id="current-day"><<print $game_state.time_state.current_day>></span>
</div>
<div class="control-line">
<button class="time-btn" onclick="waitTime(10)" title="Advance 10 minutes">></button> | <button class="time-btn" onclick="waitTime(60)" title="Advance 1 hour">>></button> | <button class="time-btn" onclick="waitTime(1440)" title="Advance 1 day">>>>>></button>
</div>
</div>
<</widget>>
<<widget "sidebarItems">>
<<if setup.sidebar_items && setup.sidebar_items.length > 0>>
<div id="sidebar-items-widget">
<<for _si to 0; _si lt setup.sidebar_items.length; _si++>>
<<set _item to setup.sidebar_items[_si]>>
<<if _item.show_when and not setup.triggerConditionsSatisfied(_item.show_when)>>
<<continue>>
<</if>>
<<if _item.type is "countdown">>
<<set _daysLeft to _item.total_days - $game_state.time_state.day + 1>>
<div class="sidebar-item countdown-item" id="sidebar-countdown-<<print _si>>">
<<if _daysLeft gt 1>>
<<print _daysLeft + " " + _item.label>>
<<elseif _daysLeft is 1>>
<<print "Tomorrow is " + _item.label.replace("days until ", "")>>
<<elseif _daysLeft is 0>>
<<print "Today is " + _item.label.replace("days until ", "")>>
<<else>>
<<print _item.label.replace("days until ", "") + " has passed">>
<</if>>
</div>
<<elseif _item.type is "trait_bar">>
<<set _tbOwner to _item.trait_owner || "player">>
<<set _tbKey to _item.trait>>
<<if _tbOwner is "npc">>
<<set _tbNpcId to _item.npc_id>>
<<set _tbNpcObj to (setup.npc_slug_map && setup.npc_slug_map[_tbNpcId]) ? State.variables.npcs[setup.npc_slug_map[_tbNpcId]] : (State.variables.npcs ? State.variables.npcs[_tbNpcId] : null)>>
<<set _traitVal to (_tbNpcObj && _tbNpcObj.core_traits) ? (_tbNpcObj.core_traits[_tbKey] || 0) : 0>>
<<else>>
<<set _traitVal to ($player && $player.core_traits) ? ($player.core_traits[_tbKey] || 0) : 0>>
<</if>>
<<set _traitMax to _item.max || 100>>
<<set _traitLabel to _item.label || _tbKey>>
<<set _traitPct to Math.max(0, Math.min(100, (_traitVal / _traitMax) * 100))>>
<<set _tbTier to "">>
<<if _item.color_tiers>>
<<for _ti to 0; _ti lt _item.color_tiers.length; _ti++>>
<<if _tbTier is "" and _traitPct lte (_item.color_tiers[_ti].up_to)>>
<<set _tbTier to _item.color_tiers[_ti].class>>
<</if>>
<</for>>
<</if>>
<<set _tbBandText to "">>
<<set _tbBandIcon to "">>
<<if _item.bands>>
<<for _bi to 0; _bi lt _item.bands.length; _bi++>>
<<set _bb to _item.bands[_bi]>>
<<if _tbBandText is "" and _bb.min isnot undefined and _bb.max isnot undefined and _traitVal gte _bb.min and _traitVal lte _bb.max>>
<<set _tbBandText to _bb.text>>
<<set _tbBandIcon to _bb.icon || "">>
<</if>>
<</for>>
<</if>>
<div class="sidebar-item trait-bar-item" id="sidebar-trait-bar-<<print _si>>">
<div class="trait-bar-label">
<<if _item.hide_value is true>>
<<print _traitLabel>>
<<else>>
<<print _traitLabel>>: <<print Math.floor(_traitVal)>> / <<print _traitMax>>
<</if>>
</div>
<div class="trait-bar-bg">
<div class="trait-bar-fill <<print _tbTier>>" style="width: <<print _traitPct>>%"></div>
<<if _tbBandText isnot "">>
<span class="trait-bar-band-text"><<print (_tbBandIcon ? _tbBandIcon + " " : "") + _tbBandText>></span>
<</if>>
</div>
</div>
<<elseif _item.type is "trait_status_text">>
<<set _tsOwner to _item.trait_owner || "player">>
<<set _tsKey to _item.trait>>
<<if _tsOwner is "npc">>
<<set _tsNpcId to _item.npc_id>>
<<set _tsNpcObj to (setup.npc_slug_map && setup.npc_slug_map[_tsNpcId]) ? State.variables.npcs[setup.npc_slug_map[_tsNpcId]] : (State.variables.npcs ? State.variables.npcs[_tsNpcId] : null)>>
<<set _tsVal to (_tsNpcObj && _tsNpcObj.core_traits) ? (_tsNpcObj.core_traits[_tsKey] || 0) : 0>>
<<else>>
<<set _tsVal to ($player && $player.core_traits) ? ($player.core_traits[_tsKey] || 0) : 0>>
<</if>>
<<set _tsText to "">>
<<set _tsIcon to "">>
<<if _item.bands>>
<<for _bi to 0; _bi lt _item.bands.length; _bi++>>
<<set _bb to _item.bands[_bi]>>
<<set _bMin to (_bb.min isnot undefined) ? _bb.min : -1e9>>
<<set _bMax to (_bb.max isnot undefined) ? _bb.max : 1e9>>
<<if _tsText is "" and _tsVal gte _bMin and _tsVal lte _bMax>>
<<set _tsText to _bb.text>>
<<set _tsIcon to _bb.icon || "">>
<</if>>
<</for>>
<</if>>
<<if _tsText isnot "">>
<div class="sidebar-item trait-status-text-item">
<<if _item.label>><div class="band-header"><<print _item.label>></div><</if>>
<span class="band-value"><<print (_tsIcon ? _tsIcon + " " : "") + _tsText>></span>
</div>
<</if>>
<<elseif _item.type is "trait_decay_warning">>
<!-- E20: render an amber warning when a decaying trait dropped today AND
is within 2.0 of its next stage gate. setup.getDecayWarnings() walks
the configured thresholds and compares last_day_snapshot vs current. -->
<<set _decayWarnings to setup.getDecayWarnings(_item.thresholds || {})>>
<<for _dw range _decayWarnings>>
<div class="sidebar-item trait-decay-warning-item">
⚠ <<print _dw.text>>
</div>
<</for>>
<<elseif _item.type is "passes">>
<div class="sidebar-item passes-item" id="sidebar-passes-<<print _si>>">
<<for _pi to 0; _pi lt setup.passes.length; _pi++>>
<<set _pass to setup.passes[_pi]>>
<<set _remaining to setup.getPassDaysRemaining(_pass.id)>>
<<if _remaining gte 0>>
<div class="pass-entry pass-active" id="pass-display-<<print _pass.id>>">
<<print (_pass.icon || "") + " " + _pass.name + ": " + _remaining + "d">>
</div>
<</if>>
<</for>>
</div>
<<elseif _item.type is "inventory">>
<div class="sidebar-item inventory-item" id="sidebar-inventory-<<print _si>>">
<<for _ii to 0; _ii lt setup.items.length; _ii++>>
<<set _itm to setup.items[_ii]>>
<<set _count to setup.getItemCount(_itm.id)>>
<<if _count gt 0>>
<div class="inventory-entry" id="inventory-display-<<print _itm.id>>">
<<print (_itm.icon || "") + " " + _itm.name + ": " + _count>>
</div>
<</if>>
<</for>>
</div>
<<elseif _item.type is "trait_words">>
<<set _twOwner to _item.trait_owner || "player">>
<<set _twKey to _item.trait>>
<<if _twOwner is "npc">>
<<set _twNpcId to _item.npc_id>>
<<set _twNpcObj to (setup.npc_slug_map && setup.npc_slug_map[_twNpcId]) ? State.variables.npcs[setup.npc_slug_map[_twNpcId]] : (State.variables.npcs ? State.variables.npcs[_twNpcId] : null)>>
<<set _twVal to (_twNpcObj && _twNpcObj.core_traits) ? (_twNpcObj.core_traits[_twKey] || 0) : 0>>
<<else>>
<<set _twVal to ($player && $player.core_traits) ? ($player.core_traits[_twKey] || 0) : 0>>
<</if>>
<<set _twMatched to "">>
<<set _twFound to false>>
<<if _item.bands>>
<<for _bi to 0; _bi lt _item.bands.length; _bi++>>
<<if not _twFound>>
<<set _twBand to _item.bands[_bi]>>
<<if _twBand.flag>>
<<if $flags and $flags[_twBand.flag] is true>>
<<set _twMatched to _twBand.text>>
<<set _twFound to true>>
<</if>>
<<elseif _twBand.min isnot undefined and _twBand.max isnot undefined>>
<<if _twVal gte _twBand.min and _twVal lte _twBand.max>>
<<set _twMatched to _twBand.text>>
<<set _twFound to true>>
<</if>>
<</if>>
<</if>>
<</for>>
<</if>>
<<if _twMatched isnot "">>
<div class="sidebar-item trait-words-item" id="sidebar-trait-words-<<print _si>>">
<<if _item.label>><div class="band-header"><<print _item.label>></div><</if>>
<span class="band-value"><<print _twMatched>></span>
</div>
<</if>>
<<elseif _item.type is "stage_label">>
/* E11: render "<prefix>: <stage label>" sourced from arc_stages.
Stage value lives in $player.core_traits[<slug>_stage] (integer).
Out-of-range value → highest-defined label. Undefined trait → just
the prefix with no colon. Empty arc_stages → renders nothing. */
<<set _slNpcId to _item.npc_id>>
<<set _slStages to (setup.npc_arc_stages && setup.npc_arc_stages[_slNpcId]) ? setup.npc_arc_stages[_slNpcId] : []>>
<<set _slRawStage to ($player && $player.core_traits) ? $player.core_traits[_slNpcId + "_stage"] : undefined>>
<<set _slPrefix to _item.prefix>>
<<if not _slPrefix>>
<<set _slUuid to (setup.npc_slug_map && setup.npc_slug_map[_slNpcId]) ? setup.npc_slug_map[_slNpcId] : _slNpcId>>
<<set _slNpcObj to State.variables.npcs ? State.variables.npcs[_slUuid] : null>>
<<set _slPrefix to _slNpcObj ? (_slNpcObj.name || _slNpcId) : _slNpcId>>
<</if>>
<<if _slStages.length gt 0 and _slRawStage isnot undefined>>
<<set _slIdx to Math.max(0, Math.min(Number(_slRawStage), _slStages.length - 1))>>
<<set _slLabel to _slStages[_slIdx]>>
<div class="sidebar-item stage-label-item" id="sidebar-stage-label-<<print _si>>">
<<print _slPrefix>>: <<print _slLabel>>
</div>
<<elseif _slPrefix>>
<div class="sidebar-item stage-label-item" id="sidebar-stage-label-<<print _si>>">
<<print _slPrefix>>
</div>
<</if>>
<<elseif _item.type is "npc_panel">>
/* RTS-style per-NPC card: name header + rows (arousal band / corruption / location).
Location uses setup.getNpcLocation — the SAME schedule source the Schedule page uses. */
<<set _npId to _item.npc_id>>
<<set _npObj to (setup.npc_slug_map && setup.npc_slug_map[_npId]) ? State.variables.npcs[setup.npc_slug_map[_npId]] : (State.variables.npcs ? State.variables.npcs[_npId] : null)>>
<<if _npObj>>
<<set _npRows to _item.rows || ["arousal"]>>
<<set _npHeader to _item.label || _npObj.name || _npId>>
<div class="sidebar-item npc-panel-item" id="sidebar-npc-panel-<<print _si>>">
<div class="npc-panel-header"><<print _npHeader>></div>
<<for _npri to 0; _npri lt _npRows.length; _npri++>>
<<set _npRow to _npRows[_npri]>>
<<if _npRow is "arousal">>
<<if not (setup.hiddenTraits && setup.hiddenTraits.includes("arousal"))>>
<<set _npAr to (_npObj.core_traits) ? (_npObj.core_traits.arousal || 0) : 0>>
<<set _npBands to _item.arousal_bands || [{"min":0,"max":0,"text":"❄️"},{"min":1,"max":1,"text":"🔥"},{"min":2,"max":2,"text":"🔥🔥"},{"min":3,"max":3,"text":"🔥🔥🔥"}]>>
<<set _npArText to "">>
<<for _nbi to 0; _nbi lt _npBands.length; _nbi++>>
<<set _nb to _npBands[_nbi]>>
<<if _npArText is "" and _nb.min isnot undefined and _nb.max isnot undefined and _npAr gte _nb.min and _npAr lte _nb.max>>
<<set _npArText to _nb.text>>
<</if>>
<</for>>
<div class="sidebar-row"><span class="sidebar-label">🔥 Arousal</span> <span class="sidebar-value"><<print _npArText>></span></div>
<</if>>
<<elseif _npRow is "corruption">>
<<if not (setup.hiddenTraits && setup.hiddenTraits.includes("corruption"))>>
<<set _npCorr to (_npObj.core_traits) ? (_npObj.core_traits.corruption || 0) : 0>>
<<set _npCorrOut to _npCorr>>
<<if _item.corruption_max_value isnot undefined and _npCorr gte _item.corruption_max_value>>
<<set _npCorrOut to (_item.corruption_max_label || "MAX")>>
<</if>>
<div class="sidebar-row"><span class="sidebar-label">🫦 Corruption</span> <span class="sidebar-value"><<print _npCorrOut>></span></div>
<</if>>
<<elseif _npRow is "location">>
<<set _npLoc to setup.getNpcLocation(_npId)>>
<<set _npLocName to _npLoc ? (setup._locNameFromUuid(_npLoc.location) || _npLoc.location) : (_item.away_label || "Away")>>
<div class="sidebar-row"><span class="sidebar-label">📍 Location</span> <span class="sidebar-value"><<print _npLocName>></span></div>
<<elseif _npRow is "next">>
/* Next-milestone block — EXACT Quests-page parity: reuses setup.renderQuestsGoalBlock, so
the card shows the same goal block as the Quests page — 🎯 To advance + ◯ live progress
(climbing) / 🔓 Ready + 📍 + 🕒 (ready) / ✓ Arc complete (terminal). No flavor/tip text. */
<<if setup.pickQuestsCard and setup.renderQuestsGoalBlock>>
<<set _npCard to setup.pickQuestsCard(_npId)>>
<<if _npCard>>
<<set _npGoalBlock to setup.renderQuestsGoalBlock(_npCard, setup.evaluateGoals(_npCard))>>
<<if _npGoalBlock>>
<div class="npc-panel-next"><<print _npGoalBlock>></div>
<</if>>
<</if>>
<</if>>
<</if>>
<</for>>
</div>
<</if>>
<</if>>
<</for>>
</div>
<</if>>
<</widget>>
<<widget "playerPortrait">>
<<if setup.player_portrait_enabled and setup.player_portrait>>
<<set _pimg to setup.getPlayerPortrait()>>
<<if _pimg>>
<div class="sidebar-player-portrait"><img @src="_pimg" @alt="$player.name" onerror="this.style.display='none';"></div>
<</if>>
<</if>>
<</widget>>
<<widget "activeModifiers">>
<<if $game_state && $game_state.active_modifiers && Object.keys($game_state.active_modifiers).length gt 0>>
<div id="sidebar-modifiers-widget" class="traits-display">
<div class="traits-header">Active Effects</div>
<<set _modKeys to Object.keys($game_state.active_modifiers)>>
<<for _mi to 0; _mi lt _modKeys.length; _mi++>>
<<set _mod to $game_state.active_modifiers[_modKeys[_mi]]>>
<<set _hoursLeft to (_mod.expires_day - $game_state.time_state.day) * 24 + (_mod.expires_hour - $game_state.time_state.current_hour)>>
<div class="modifier-badge"><<print _mod.name>> (<<print _hoursLeft>>h)</div>
<</for>>
</div>
<</if>>
<</widget>>
<<widget "inc">>
<<set _incTrait to $args[0]>>
<<set _incBy to ($args[1] != null) ? Number($args[1]) : 1>>
<<script>>setup.applyAndNotifyTrait("player", null, _incTrait, "add", _incBy, false, null);<</script>>
<</widget>>
<<widget "dec">>
<<set _decTrait to $args[0]>>
<<set _decBy to ($args[1] != null) ? Number($args[1]) : 1>>
<<script>>setup.applyAndNotifyTrait("player", null, _decTrait, "add", -_decBy, false, null);<</script>>
<</widget>>
<<widget "playerTraits">>
<div id="traits-widget" class="traits-display">
<div class="traits-header">Traits</div>
<<if $player and $player.core_traits and Object.keys($player.core_traits).length > 0>>
<<set _keys to Object.keys($player.core_traits).sort()>>
<ul class="traits-list">
<<for _i to 0; _i lt _keys.length; _i++>>
<<set _k to _keys[_i]>>
<<if setup.hiddenTraits && setup.hiddenTraits.includes(_k)>><<continue>><</if>>
<li class="trait-item">
<span class="trait-name"><<print _k>></span>
<span class="trait-value"><<print $player.core_traits[_k]>></span>
</li>
<</for>>
</ul>
<<else>>
<div class="no-traits">No traits</div>
<</if>>
<div class="traits-hint">Updates after each choice.</div>
</div>
<</widget>>
<<widget "playerFlags">>
<div id="flags-widget" class="traits-display">
<div class="traits-header">Flags</div>
<<if $flags and Object.keys($flags).length > 0>>
<<set _fkeys to Object.keys($flags).sort()>>
<ul class="traits-list">
<<for _j to 0; _j lt _fkeys.length; _j++>>
<<set _fk to _fkeys[_j]>>
<li class="trait-item">
<span class="trait-name"><<print _fk>></span>
<span class="trait-value"><<print $flags[_fk] ? '✔' : '✖'>></span>
</li>
<</for>>
</ul>
<<else>>
<div class="no-traits">No flags</div>
<</if>>
<div class="traits-hint">Set by choices and story logic.</div>
</div>
<</widget>>
<<widget "questsButton">>
<!-- Debug/legacy quest view - shows raw mechanics -->
<div id="quests-btn-widget">
<<if passage() isnot "QuestsPage">><<link "Quests" "QuestsPage">><</link>><<else>><span class="nav-row">Quests</span><</if>><span class="nav-i">📋</span>
</div>
<</widget>>
<<widget "missingMediaButton">>
<<if $flags.debug_mode>>
<div id="missing-media-btn-widget" style="margin-bottom:8px;">
<<if passage() isnot "MissingMediaPage">><<link "⚠️ Missing Media" "MissingMediaPage">><</link>><<else>>⚠️ Missing Media<</if>>
</div>
<</if>>
<</widget>>
<<widget "journalButton">>
<!-- Quests - active per-NPC quest cards -->
<div id="journal-btn-widget">
<<if passage() isnot "QuestsPage">><<link "Quests" "QuestsPage">><</link>><<else>><span class="nav-row">Quests</span><</if>><span class="nav-i">📋</span>
</div>
<</widget>>
<<widget "tipsButton">>
<!-- Tips - game-level mechanics surface; conditional on [ui.tips_page] authored -->
<<if setup.tips_page && setup.tips_page.content>>
<div id="tips-btn-widget">
<<if passage() isnot "TipsPage">><<link "Tips" "TipsPage">><</link>><<else>><span class="nav-row">Tips</span><</if>><span class="nav-i">💡</span>
</div>
<</if>>
<</widget>>
<<widget "statsButton">>
<div id="stats-btn-widget">
<<if passage() isnot "StatsPage">><<link "Stats" "StatsPage">><</link>><<else>><span class="nav-row">Stats</span><</if>><span class="nav-i">📊</span>
</div>
<</widget>>
<<widget "scheduleButton">>
<<set _npcsWithSchedules to setup.getNpcsWithSchedules()>>
<<set _soloActivities to setup.getSoloActivitiesForToday()>>
<<if _npcsWithSchedules.length > 0 || _soloActivities.length > 0>>
<div id="schedule-btn-widget">
<<if passage() isnot "SchedulePage">><<link "Schedules" "SchedulePage">><</link>><<else>><span class="nav-row">Schedules</span><</if>><span class="nav-i">📅</span>
</div>
<</if>>
<</widget>>
<<widget "patreonButton">>
<div id="patreon-btn-widget">
<a href="https://www.patreon.com/cw/nutgames844" target="_blank" rel="noopener" class="patreon-link">
<svg class="patreon-icon" viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M15.386.524c-4.764 0-8.64 3.876-8.64 8.64 0 4.75 3.876 8.613 8.64 8.613 4.75 0 8.614-3.864 8.614-8.613C24 4.4 20.136.524 15.386.524M.003 23.537h4.22V.524H.003"/>
</svg>
Support Us
</a>
</div>
<</widget>>
<<widget "flagsButton">>
<div id="flags-btn-widget">
<<if passage() isnot "FlagsPage">><<link "Flags" "FlagsPage">><</link>><<else>><span class="nav-row">Flags</span><</if>><span class="nav-i">🚩</span>
</div>
<</widget>>
<<widget "versionFooter">>
<div class="sidebar-version">v0.1.6 · 2026-07-28 · <span class="build-badge">Free Build</span></div>
<</widget>><<missingMediaButton>>
<<timeDisplay>>
<<playerPortrait>>
<<sidebarItems>>
<<activeModifiers>>
<<journalButton>>
<<tipsButton>>
<<cheatButton>>
<<statsButton>>
<<scheduleButton>>
<<playerTraits>>
<<patreonButton>>
<<versionFooter>>
<style>
#time-widget {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 4px;
padding: 8px;
margin-bottom: 10px;
font-family: var(--theme-font-mono);
font-size: 14px;
line-height: 1.2;
color: var(--theme-text);
}
.time-line {
text-align: center;
margin-bottom: 4px;
font-weight: bold;
color: var(--theme-text-strong);
}
.control-line {
text-align: center;
font-size: 12px;
}
.time-btn {
background: var(--theme-surface-alt);
border: 1px solid var(--theme-border);
border-radius: 3px;
color: var(--theme-text-secondary);
cursor: pointer;
padding: 3px 6px;
font-size: 12px;
font-family: var(--theme-font-mono);
font-weight: bold;
}
.time-btn:hover {
background: var(--theme-border);
color: var(--theme-text-strong);
border-color: var(--theme-border);
}
#sidebar-items-widget {
margin-bottom: 8px;
}
.sidebar-item {
text-align: left; /* one type system across the rail — left, theme sans (was centered mono) */
padding: 6px 8px;
font-size: 13px;
line-height: 1.3;
}
.countdown-item {
background: var(--theme-warning-bg);
border: 1px solid var(--theme-warning);
border-radius: 4px;
color: var(--theme-warning-text);
font-weight: bold;
}
.hint-item {
background: var(--theme-success-bg);
border: 1px solid var(--theme-success);
border-radius: 4px;
color: var(--theme-success);
font-style: italic;
}
#traits-widget {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 4px;
padding: 8px;
margin-top: 8px;
font-family: var(--theme-font-mono);
font-size: 13px;
color: var(--theme-text);
}
#flags-widget {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 4px;
padding: 8px;
margin-top: 8px;
font-family: var(--theme-font-mono);
font-size: 13px;
color: var(--theme-text);
}
.traits-header {
font-weight: bold;
margin-bottom: 6px;
}
.traits-list {
list-style: none;
padding-left: 0;
margin: 0;
}
.trait-item {
display: flex;
justify-content: space-between;
border-bottom: 1px dashed var(--theme-border);
padding: 2px 0;
}
.trait-name {
color: var(--theme-text-secondary);
}
.trait-value {
color: var(--theme-primary);
font-weight: bold;
}
.no-traits {
font-style: italic;
color: var(--theme-text-muted);
}
.traits-hint {
margin-top: 6px;
font-size: 11px;
color: var(--theme-text-muted);
}
#time-display, #current-day {
font-family: var(--theme-font-mono);
color: var(--theme-text-strong);
}
/* ===== Sidebar action buttons — ONE neutral pill family =====
Structure is carried by neutral surfaces; the accent is reserved for hover
(and .is-active). Replaces the old zoo: 4 tinted pills + 2 parchment-serif
journal/tips. All six now share one filled surface pill, left-aligned, muted
label, accent on hover, single 6px radius. */
/* ===== Sidebar action buttons — ONE neutral pill family =====
The link (or the current-page .nav-row span) FILLS the whole padded box, so
the ENTIRE box is clickable — not just the text. The icon overlays the right
edge with pointer-events:none, so clicks there pass through to the link beneath.
Structure in neutrals; accent reserved for hover. */
#phone-sidebar-btn,
#quests-btn-widget,
#journal-btn-widget,
#tips-btn-widget,
#cheat-btn-widget,
#stats-btn-widget,
#schedule-btn-widget,
#flags-btn-widget {
position: relative; /* anchor the icon overlay + unread badge */
background: var(--theme-surface);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 6px;
margin: 8px 0 0 0; /* uniform 8px rhythm — same as the cards */
}
/* the clickable fill: <a> in nav state, .nav-row span on the current page */
#phone-sidebar-btn a,
#quests-btn-widget a, #journal-btn-widget a, #tips-btn-widget a, #cheat-btn-widget a, #stats-btn-widget a, #schedule-btn-widget a, #flags-btn-widget a,
#quests-btn-widget .nav-row, #journal-btn-widget .nav-row, #tips-btn-widget .nav-row, #cheat-btn-widget .nav-row, #stats-btn-widget .nav-row, #schedule-btn-widget .nav-row, #flags-btn-widget .nav-row {
display: block;
width: 100%;
box-sizing: border-box;
padding: 8px 30px 8px 10px; /* padded fill → whole box clickable; right room for the icon */
text-align: left; /* override the #ui-bar default center */
color: var(--theme-text-muted);
text-decoration: none;
font-weight: 600;
}
#phone-sidebar-btn a:hover,
#quests-btn-widget a:hover, #journal-btn-widget a:hover, #tips-btn-widget a:hover, #cheat-btn-widget a:hover, #stats-btn-widget a:hover, #schedule-btn-widget a:hover, #flags-btn-widget a:hover {
color: var(--theme-accent);
text-decoration: none;
}
.nav-i { /* right-edge icon overlay; clicks pass through to the link */
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
/* Sidebar spacing is margin-driven, not <br>-driven (kill stray empty-widget breaks). */
#story-caption br { display: none; }
/* State-reactive player portrait (opt-in) — the TOP-MOST sidebar image. The theme's
global img rule does not reach sidebar imgs, so without this the raw image renders at
natural size and overflows the narrow sidebar (you'd see a background edge, not the
face). Constrain to the sidebar width and frame it as a 3:4 portrait crop centred a bit
high, so a square or tall source shows the face/torso rather than the legs/background. */
.sidebar-player-portrait {
margin: 0 0 12px 0;
text-align: center;
}
.sidebar-player-portrait img {
display: block;
width: 100%;
max-width: 100%;
height: auto;
aspect-ratio: 3 / 4;
object-fit: cover;
object-position: 50% 18%;
border-radius: 6px;
}
/* Patreon Button in Sidebar */
#patreon-btn-widget {
margin: 15px 0 0 0;
padding: 15px 0 0 0;
border-top: 1px solid #444;
text-align: center;
}
/* Version / release-date footer at the very bottom of the sidebar */
.sidebar-version {
margin-top: 8px;
font-size: 11px;
color: var(--theme-text-muted, #8a8a8a);
text-align: center;
}
/* Cheat page rows. Flat and mechanical by design — this page is read as a list of
levers, not as prose, so the fiction lives on the container and the rows stay legible. */
.cheat-intro {
margin-bottom: 12px;
color: var(--theme-text-muted, #8a8a8a);
font-size: 13px;
}
.cheat-row {
padding: 7px 0;
border-bottom: 1px solid var(--theme-border, #2a2a2a);
}
.cheat-row:last-of-type { border-bottom: none; }
.cheat-row.is-locked {
display: flex;
align-items: center;
justify-content: space-between;
opacity: 0.55;
}
.cheat-label { font-weight: 600; }
.cheat-lock { font-size: 12px; }
.cheat-hint, .cheat-at-cap {
margin-top: 2px;
color: var(--theme-text-muted, #8a8a8a);
font-size: 12px;
}
.cheat-row-maxed {
font-weight: 600;
opacity: 0.5;
}
.cheat-locked-note {
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid var(--theme-border, #2a2a2a);
color: var(--theme-text-muted, #8a8a8a);
font-size: 12px;
}
/* Build badge — names which file the player is holding (free vs supporter build).
Rides inside .sidebar-version and is reused on the cheat page's heading. */
.build-badge {
display: inline-block;
padding: 0 5px;
border: 1px solid var(--theme-border, #3a3a3a);
border-radius: 3px;
font-size: 10px;
letter-spacing: 0.02em;
white-space: nowrap;
color: var(--theme-text-muted, #8a8a8a);
}
/* Reduce gap between StoryCaption and Save/Restart menu */
#story-caption {
margin-bottom: 0 !important;
padding-bottom: 0 !important;
}
#menu-story {
margin-top: 8px !important;
}
.patreon-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: #f96854;
color: #fff !important;
border-radius: 4px;
text-decoration: none;
font-size: 0.9em;
font-weight: bold;
}
.patreon-link:hover {
background: #e85a47;
text-decoration: none !important;
}
.patreon-icon {
flex-shrink: 0;
}
.stats-card {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
}
.stats-name {
font-weight: bold;
font-size: 1.2em;
color: var(--theme-text);
margin-bottom: 8px;
border-bottom: 1px solid var(--theme-border);
padding-bottom: 6px;
}
.stats-traits {
margin-top: 8px;
}
.stats-trait-item {
display: flex;
justify-content: space-between;
padding: 4px 8px;
border-bottom: 1px dashed var(--theme-border);
color: var(--theme-text-strong);
}
.stats-trait-value {
font-weight: bold;
color: var(--theme-primary);
}
/* Story Arc Help Page Styles */
.chapter-context {
background: linear-gradient(135deg, var(--theme-surface) 0%, var(--theme-surface-alt) 100%);
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chapter-name {
font-size: 1.1em;
color: var(--theme-text);
font-weight: bold;
}
.chapter-mood {
font-size: 0.9em;
color: var(--theme-text-muted);
font-style: italic;
}
.available-section h3 {
font-size: 1em;
color: var(--theme-text-secondary);
margin: 0 0 10px 0;
border-bottom: 1px solid var(--theme-border);
padding-bottom: 6px;
}
.help-card {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 10px;
}
.node-name {
font-weight: bold;
color: var(--theme-text);
}
.node-hint {
color: var(--theme-text-muted);
font-size: 0.9em;
margin-top: 4px;
}
.narrative-hint {
background: var(--theme-warning-bg);
border: 1px solid var(--theme-warning);
border-radius: 6px;
padding: 12px;
margin: 16px 0;
}
.narrative-hint p {
margin: 0;
color: var(--theme-warning-text);
}
.progress-summary {
text-align: center;
color: var(--theme-text-muted);
font-size: 0.9em;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--theme-border);
}
.no-quests-msg {
font-style: italic;
color: var(--theme-text-muted);
padding: 10px;
}
/* Quest Page - Simplified One Activity Per NPC */
.npc-section {
margin-bottom: 20px;
padding: 16px;
background: linear-gradient(135deg, var(--theme-surface) 0%, var(--theme-surface-alt) 100%);
border-radius: 10px;
border: 1px solid var(--theme-border);
}
.npc-name {
font-size: 1.2em;
color: var(--theme-text);
margin: 0 0 12px 0;
border-bottom: 2px solid var(--theme-primary);
padding-bottom: 8px;
font-weight: 600;
}
.guide-hint {
font-size: 0.85em;
color: #f0c040;
font-style: italic;
}
.quest-available {
color: var(--theme-success);
font-weight: 500;
padding: 10px 14px;
background: rgba(40, 167, 69, 0.1);
border-radius: 6px;
border-left: 4px solid var(--theme-success);
}
/* E16: Stage hint two-part card. Flavor on top, goal block below. */
.stage-hint-card {
padding: 12px 14px;
background: rgba(40, 167, 69, 0.08);
border-radius: 6px;
border-left: 4px solid var(--theme-success);
margin: 8px 0;
}
.stage-hint-flavor {
font-style: italic;
opacity: 0.75;
margin-bottom: 10px;
font-size: 0.95em;
}
/* Pattern 2 (2026-05-01) — auto-rendered structured goal block */
.stage-hint-goal-header {
font-weight: 600;
margin-bottom: 6px;
color: var(--theme-success);
}
.stage-hint-goal ul {
list-style: none;
padding-left: 8px;
margin: 4px 0;
}
.stage-hint-goal li {
padding: 3px 0;
font-family: var(--theme-font-mono, 'Courier New', monospace);
font-size: 0.92em;
line-height: 1.4;
}
.stage-hint-met {
color: var(--theme-success);
font-weight: 600;
display: inline-block;
width: 1.2em;
}
.stage-hint-unmet {
color: var(--theme-text-muted);
display: inline-block;
width: 1.2em;
}
.stage-hint-met-row {
color: var(--theme-success);
opacity: 0.9;
}
.stage-hint-unmet-row {
color: var(--theme-text);
opacity: 0.85;
}
.stage-hint-progress {
color: var(--theme-text-muted);
margin-left: 6px;
font-size: 0.88em;
}
.stage-hint-where {
margin-top: 8px;
font-size: 0.9em;
opacity: 0.85;
color: var(--theme-text);
font-style: normal;
}
.stage-hint-tip {
margin-top: 10px;
padding: 6px 10px;
background: rgba(255, 193, 7, 0.08);
border-left: 3px solid var(--theme-warning, #ffc107);
border-radius: 3px;
font-size: 0.9em;
line-height: 1.45;
opacity: 0.92;
}
.stage-hint-path {
margin-top: 6px;
padding: 6px 8px;
border-left: 2px solid var(--theme-accent, #4ecdc4);
background: rgba(78, 205, 196, 0.05);
}
.stage-hint-path strong {
color: var(--theme-accent, #4ecdc4);
font-size: 0.92em;
}
/* Pattern 2 (2026-05-04) — Ready frame: shown when every gate of a stage
transition's trigger evaluates true. Replaces the gate checklist with a
compact "go here, this time window" surface. */
.stage-hint-ready {
color: var(--theme-success);
font-weight: 700;
letter-spacing: 0.02em;
}
/* Pattern 2 (2026-05-09) — Arc-complete frame: shown when an author marks
a hint template `arc_complete = true` (terminal stage in the slice).
Quieter than Ready — reads as closure rather than an action prompt. */
.stage-hint-arc-complete {
color: var(--theme-text-muted, #888);
font-weight: 600;
font-style: italic;
letter-spacing: 0.02em;
}
.quest-locked {
color: var(--theme-warning-text);
font-weight: 500;
padding: 10px 14px;
background: rgba(255, 193, 7, 0.15);
border-radius: 6px;
border-left: 4px solid var(--theme-warning);
}
.quest-conditions {
color: var(--theme-danger);
font-weight: 500;
padding: 10px 14px;
background: rgba(220, 53, 69, 0.1);
border-radius: 6px;
border-left: 4px solid var(--theme-danger);
}
.quest-waiting {
color: var(--theme-warning-text);
font-weight: 500;
padding: 10px 14px;
background: rgba(255, 193, 7, 0.15);
border-radius: 6px;
border-left: 4px solid var(--theme-warning);
font-style: italic;
}
.dev-canvas-info {
background: var(--theme-primary);
color: white;
padding: 6px 10px;
border-radius: 4px;
margin-bottom: 10px;
font-size: 11px;
font-family: var(--theme-font-mono);
}
.quest-complete {
color: var(--theme-success);
font-style: italic;
padding: 10px 14px;
text-align: center;
}
.no-quests {
color: var(--theme-text-muted);
font-style: italic;
padding: 12px;
text-align: center;
}
/* Tips page — wrapped inside an .npc-section card frame on the page itself.
Section headers (<h3>) use the same accent treatment as .npc-name dividers
in the Quests page so the two pages read as the same family. */
.tips-page-content {
line-height: 1.55;
color: var(--theme-text);
}
.tips-page-content h3 {
font-size: 1.05em;
color: var(--theme-text);
margin: 18px 0 8px;
padding-bottom: 6px;
border-bottom: 2px solid var(--theme-primary);
font-weight: 600;
}
.tips-page-content h3:first-child {
margin-top: 0;
}
.tips-page-content p {
margin: 6px 0;
}
.tips-page-content strong {
color: var(--theme-text);
font-weight: 600;
}
/* Trait Requirement Link */
.trait-requirement-link {
color: var(--theme-danger);
text-decoration: underline;
cursor: pointer;
}
.trait-requirement-link:hover {
color: var(--theme-danger);
}
/* Trait Activities Modal Overlay */
.trait-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
/* Modal Container */
.trait-modal {
background: #fff;
border-radius: 12px;
max-width: 400px;
width: 90%;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.trait-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: var(--theme-surface);
border-bottom: 1px solid var(--theme-border);
}
.trait-modal-header h3 {
margin: 0;
font-size: 1.1em;
color: #333;
}
.trait-modal-close {
font-size: 24px;
cursor: pointer;
color: var(--theme-text-muted);
line-height: 1;
}
.trait-modal-close:hover {
color: #333;
}
.trait-modal-progress {
padding: 12px 20px;
background: rgba(220, 53, 69, 0.1);
color: var(--theme-danger);
font-weight: 500;
text-align: center;
}
.trait-modal-body {
padding: 16px 20px;
max-height: 50vh;
overflow-y: auto;
}
/* Activity List */
.trait-activity-list {
list-style: none;
padding: 0;
margin: 0;
}
.trait-activity-list li {
padding: 12px;
border-bottom: 1px solid var(--theme-border);
}
.trait-activity-list li:last-child {
border-bottom: none;
}
.activity-name {
font-weight: 600;
color: #333;
}
.activity-bonus {
float: right;
color: var(--theme-success);
font-weight: 500;
}
.activity-hint {
margin-top: 4px;
color: var(--theme-text-muted);
font-size: 0.9em;
clear: both;
}
/* Effect Toast Notification */
.effect-toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.85);
color: var(--theme-success);
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
z-index: 10002;
white-space: nowrap;
}
/* S4 — Threshold-publisher toast (gated_action). Distinct from the green
effect-toast: amber/warning palette, italic, allows wrapping for the longer
in-character explanation, slightly higher z-index so it doesn't get hidden
behind the success toast when both fire on the same click. */
.effect-toast.notify-warning {
background: rgba(255, 152, 0, 0.95);
color: #1a1a1a;
border-left: 4px solid #c66900;
font-style: italic;
white-space: normal;
max-width: 480px;
text-align: left;
line-height: 1.4;
z-index: 10003;
}
/* Game Intro - Top Left Aligned */
.game-intro {
text-align: left;
}
/* Age Gate Section */
.age-gate {
background: var(--theme-warning-bg);
border: 1px solid var(--theme-warning);
border-radius: 8px;
padding: 20px;
margin: 20px 0;
text-align: center;
}
.age-warning {
font-weight: bold;
color: var(--theme-warning-text);
margin-bottom: 15px;
}
.age-buttons {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
/* Developer Intro Section (above age gate) */
.developer-intro {
text-align: left;
color: var(--theme-text-muted);
margin: 20px 0;
}
/* Developer Footer Section (below age gate) */
.developer-footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--theme-border);
text-align: center;
color: var(--theme-text-muted);
font-size: 0.9em;
}
.developer-intro a,
.developer-footer a {
color: #f96854;
text-decoration: none;
}
.developer-intro a:hover,
.developer-footer a:hover {
text-decoration: underline;
}
.developer-about {
line-height: 1.5;
margin: 10px 0;
}
.support-link {
margin-top: 8px;
}
.developer-footer .support-link {
font-weight: bold;
}
/* Age Blocked Page */
.blocked-page {
text-align: center;
padding: 50px;
color: var(--theme-danger);
background: var(--theme-danger-bg);
border-radius: 8px;
margin: 50px auto;
max-width: 400px;
}
/* Override SugarCube default vertical centering */
#story {
align-items: flex-start !important;
justify-content: flex-start !important;
padding-top: 1em !important;
}
.passage {
margin-top: 0 !important;
padding-top: 0 !important;
}
/* New/Unvisited Canvas Highlight */
.new-canvas a {
color: var(--theme-success) !important;
font-weight: 600;
}
.new-canvas a::before {
content: "✨ ";
}
/* Unlocked conditional choice highlight (in passages) */
.unlocked-choice a, .unlocked-choice .link-internal {
color: var(--theme-warning) !important;
font-weight: 600;
}
.unlocked-choice a::before, .unlocked-choice .link-internal::before {
content: "🔓 ";
}
/* Locked choice (visible but not available — Mode A) */
.locked-choice {
color: var(--theme-text-muted);
font-style: italic;
cursor: not-allowed;
opacity: 0.6;
display: inline-block;
padding: 2px 0;
}
.locked-choice::before {
content: "🔒 ";
}
/* Rejection choice (clickable but leads to rejection — Mode B) */
.rejection-choice a, .rejection-choice .link-internal {
color: #b45555 !important;
font-style: italic;
}
.rejection-choice a::before, .rejection-choice .link-internal::before {
content: "⚠️ ";
}
/* Active modifier sidebar badge */
.modifier-badge {
display: inline-block;
background: rgba(168, 85, 247, 0.15);
color: #c084fc;
border: 1px solid rgba(168, 85, 247, 0.3);
border-radius: 12px;
padding: 2px 10px;
font-size: 0.8em;
font-style: italic;
margin: 2px 0;
}
/* Unlocked choice canvas link highlight (in location activity list) */
.unlocked-choice-canvas a {
color: var(--theme-warning) !important;
font-weight: 600;
}
.unlocked-choice-canvas a::before {
content: "🔓 ";
}
/* Navigation New Content Indicator */
.nav-new {
color: var(--theme-warning);
font-weight: bold;
font-size: 1.1em;
}
/* Navigation NPC Portrait Indicator */
.nav-npc-portrait {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
vertical-align: middle;
margin-left: 4px;
border: 2px solid var(--theme-border);
}
/* ===============================================
VISUAL NAVIGATION GRID
=============================================== */
.location-nav-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin: 8px 0;
}
@media (min-width: 768px) {
.location-nav-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 1024px) {
.location-nav-grid {
grid-template-columns: repeat(4, 1fr);
}
}
/* Location Card */
.location-card {
display: flex;
flex-direction: column;
background: #2a2a2a;
border-radius: 8px;
overflow: hidden;
text-decoration: none !important;
color: inherit !important;
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
}
.location-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
/* Card Image Area (5:2 aspect ratio - more compact) */
.location-card-image {
width: 100%;
padding-bottom: 40%; /* 5:2 ratio */
background-size: cover;
background-position: center;
position: relative;
background-color: #1a1a1a;
}
/* Placeholder silhouette for locations without images */
.location-card-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #2d2d3a 0%, #1a1a2a 100%);
}
.location-card-placeholder svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60%;
height: 60%;
opacity: 0.5;
}
/* Card Content Area */
.location-card-content {
padding: 6px 8px;
display: flex;
flex-direction: column;
gap: 2px;
}
.location-card-name {
font-weight: 600;
font-size: 0.9em;
line-height: 1.2;
color: var(--theme-text);
}
/* Indicators Row */
.location-card-indicators {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
/* NEW badge (replaces ! indicator in visual mode) */
.nav-new-badge {
background: var(--theme-warning);
color: #000;
font-size: 0.65em;
font-weight: bold;
padding: 2px 5px;
border-radius: 3px;
text-transform: uppercase;
}
/* NPC portrait badges (smaller version for cards) */
.nav-npc-badge {
width: 20px;
height: 20px;
border-radius: 50%;
object-fit: cover;
border: 1px solid var(--theme-text-muted);
}
/* Locked destination card (lock-as-prose): shown but not clickable, with the reason */
.location-card-locked {
opacity: 0.6;
cursor: not-allowed;
filter: grayscale(0.4);
}
.location-card-locked .location-card-image {
filter: grayscale(0.6) brightness(0.85);
}
.nav-locked-reason {
font-size: 0.7em;
line-height: 1.25;
color: var(--theme-text-muted);
font-style: italic;
margin-top: 2px;
}
/* Text-mode locked link */
.nav-link-locked {
color: var(--theme-text-muted);
font-style: italic;
}
/* Travel-cost tag on a nav destination (neutral, informational) */
.nav-cost-tag {
font-size: 0.65em;
color: var(--theme-text-muted);
background: var(--theme-surface);
border: 1px solid var(--theme-border, rgba(255,255,255,0.12));
border-radius: 3px;
padding: 1px 5px;
margin-left: 4px;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
/* Exit Links Section (text-only, below grid) */
.location-nav-exits {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #444;
}
.location-nav-exits a {
color: var(--theme-text-muted);
}
.location-nav-exits a:hover {
color: #fff;
}
/* ===== NPC Portrait System (Location Interaction) ===== */
/* Grid of clickable NPC portraits shown at locations */
.location-npcs {
display: flex;
gap: 1.5rem;
justify-content: center;
padding: 1.5rem 0;
flex-wrap: wrap;
}
.npc-portrait-card {
text-align: center;
cursor: pointer;
transition: transform 0.2s ease;
position: relative;
}
.npc-portrait-card:hover {
transform: scale(1.08);
}
.npc-portrait-link {
text-decoration: none !important;
color: inherit !important;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
}
.npc-portrait-img {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--theme-text-muted);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.npc-portrait-card:hover .npc-portrait-img {
border-color: var(--theme-border);
box-shadow: 0 0 12px rgba(200, 200, 200, 0.2);
}
/* Initial letter fallback for missing portraits */
.npc-portrait-placeholder {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, #3a3a4a 0%, #2a2a3a 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
font-weight: 600;
color: var(--theme-border);
border: 3px solid var(--theme-text-muted);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.npc-portrait-card:hover .npc-portrait-placeholder {
border-color: var(--theme-border);
box-shadow: 0 0 12px rgba(200, 200, 200, 0.2);
}
.npc-portrait-name {
display: block;
font-size: 0.85rem;
color: var(--theme-text-muted);
line-height: 1.2;
}
/* .npc-badge kept as base class for .npc-cost-badge (cost-blocked portraits).
NEW/unlocked portrait border + badge classes removed 2026-05-25. */
.npc-badge {
position: absolute;
top: -4px;
right: -4px;
font-size: 0.6rem;
font-weight: bold;
padding: 2px 5px;
border-radius: 8px;
text-transform: uppercase;
}
/* Solo activity buttons (activities with no NPC) */
.location-solo-activities {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #333;
}
.solo-activity-btn {
display: inline-block;
padding: 0.4rem 1rem;
margin: 0.3rem 0;
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 4px;
color: var(--theme-text) !important;
text-decoration: none !important;
transition: background 0.2s ease;
}
.solo-activity-btn:hover {
background: var(--theme-surface-alt);
color: var(--theme-text-strong) !important;
}
/* ===== Cost-blocked states ===== */
/* Blocked NPC portraits (greyed out) */
.npc-portrait-blocked .npc-portrait-img,
.npc-portrait-blocked .npc-portrait-placeholder {
filter: grayscale(70%) brightness(0.6);
border-color: #444;
}
.npc-portrait-blocked .npc-portrait-name {
color: var(--theme-text-muted);
}
.npc-portrait-blocked:hover {
transform: scale(1.04);
}
/* Cost badge on portraits */
.npc-cost-badge {
background: var(--theme-danger);
color: #fff;
font-size: 0.55rem;
position: absolute;
bottom: 18px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
padding: 1px 6px;
border-radius: 8px;
top: auto;
right: auto;
}
/* Blocked solo activities */
.solo-activity-blocked {
opacity: 0.5;
}
.solo-activity-blocked:hover {
opacity: 0.7;
}
.solo-cost-tag {
font-size: 0.75em;
color: var(--theme-danger);
}
/* E21 — author-opt-in cooldown entry. Non-clickable dimmed text. */
.solo-activity-cooldown {
display: inline-block;
padding: 4px 8px;
margin-bottom: 2px;
color: var(--theme-border);
font-style: normal;
font-size: 0.9em;
opacity: 0.65;
}
.solo-activity-cooldown em {
font-size: 0.85em;
opacity: 0.85;
}
/* Cost-blocked passage message */
.cost-blocked-message {
text-align: center;
padding: 2rem;
color: var(--theme-text-muted);
font-style: italic;
}
.cost-blocked-message p {
margin-bottom: 1rem;
}
/* ===== E20: Sidebar decay warning ===== */
.trait-decay-warning-item {
margin-top: 0.5rem;
padding: 6px 10px;
background: rgba(255, 193, 7, 0.18);
border-left: 3px solid var(--theme-warning);
border-radius: 4px;
color: var(--theme-warning-text);
font-size: 0.78rem;
line-height: 1.35;
}
/* ===== Sidebar band cards (trait_words / trait_status_text / trait_bar) =====
ONE card language. Structure is carried by NEUTRAL surfaces + spacing; the
accent is RESERVED for signal (hover / .is-active), never the resting stripe.
trait_words is the HERO stat (corruption / identity) — raised surface + larger
value so it anchors the top of the rail instead of reading as a peer card. */
.trait-words-item,
.trait-status-text-item,
.trait-bar-item {
text-align: left;
margin-top: 0.5rem;
padding: 8px 10px;
background: var(--theme-surface);
border: 1px solid rgba(255, 255, 255, 0.07); /* hairline — survives across tiers, clears the eye (the old #333 border was ~1.3:1, invisible) */
border-left: 3px solid rgba(255, 255, 255, 0.10); /* neutral at rest */
border-radius: 6px;
color: var(--theme-text);
font-size: 0.78rem;
line-height: 1.35;
}
/* HERO: the identity word-state (corruption) reads as the top-of-rail anchor. */
.trait-words-item {
background: var(--theme-surface-hover); /* +0.08 lighter — the existing 'raised' tier */
}
.trait-words-item .band-value {
font-size: 1.05rem;
line-height: 1.25;
}
/* The accent/semantic left-stripe is a SCARCE SIGNAL (active / urgent / locked),
not decoration — a row earns it; toggled by emitting the class on that item. */
.sidebar-item.is-active { border-left-color: var(--theme-accent); }
.sidebar-item.is-urgent { border-left-color: var(--theme-warning); }
.sidebar-item.is-locked { border-left-color: var(--theme-danger); }
/* the label, rendered as a compact card header (matches .npc-panel-header) */
.band-header {
color: var(--theme-text-muted);
font-weight: bold;
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 4px;
}
/* the value line (word / status text) */
.band-value {
color: var(--theme-text-strong);
font-weight: 600;
font-size: 0.86rem;
font-variant-numeric: tabular-nums lining-nums; /* columns don't jitter as values change */
}
/* ===== trait_bar — banded word over a flat fill bar ===== */
.trait-bar-label {
color: var(--theme-text-muted);
font-weight: bold;
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 5px;
}
.trait-bar-bg {
height: 8px;
background: var(--theme-surface-alt);
border-radius: 6px;
overflow: hidden;
position: relative;
}
.trait-bar-fill {
height: 100%;
background: var(--theme-accent);
border-radius: 6px;
transition: width 0.3s ease;
}
/* Banded mode: when a trait_bar's band-text overlay is rendered, the bg grows
to fit the overlay text. Gated via :has() so non-banded bars stay at 8px. */
.trait-bar-bg:has(.trait-bar-band-text) {
height: 26px;
border-radius: 6px;
}
.trait-bar-band-text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 9px;
font-size: 0.78rem;
font-weight: 600;
color: var(--theme-text-strong);
font-variant-numeric: tabular-nums lining-nums;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
pointer-events: none;
z-index: 2;
white-space: nowrap;
}
/* Built-in value-tier fills — flat & matte (set a tier class via color_tiers).
Authors can override by shipping their own .trait-bar-fill.<class> rules. */
.trait-bar-fill.low {
background: #3b82c4;
}
.trait-bar-fill.medium {
background: #d98a3a;
}
.trait-bar-fill.high {
background: #d65a5a;
}
/* NPC panel card (npc_panel sidebar item) — RTS House-card: header strip + label/value rows */
.npc-panel-item {
text-align: left;
padding: 0;
margin-top: 0.5rem;
background: var(--theme-surface);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 6px;
overflow: hidden;
}
.npc-panel-header {
padding: 5px 8px;
background: var(--theme-surface-alt);
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
color: var(--theme-text-strong);
font-weight: bold;
font-size: 12px;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.npc-panel-item .sidebar-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px 8px;
font-size: 12px;
}
.npc-panel-item .sidebar-label {
color: var(--theme-text-muted);
white-space: nowrap;
}
.npc-panel-item .sidebar-value {
color: var(--theme-text-strong);
font-weight: 500;
text-align: right;
font-variant-numeric: tabular-nums lining-nums;
}
.npc-panel-next {
margin-top: 4px;
padding: 4px 8px 2px;
border-top: 1px solid rgba(255, 255, 255, 0.07);
}
/* the inner goal block reuses the Quests-page classes (.quests-goal/.quests-ready/.quests-where) */
/* Dev Mode Controls */
.dev-adj-btn {
background: var(--theme-text-muted);
color: white;
border: none;
border-radius: 3px;
width: 20px;
height: 20px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
padding: 0;
line-height: 1;
vertical-align: middle;
}
.dev-adj-btn:hover {
background: var(--theme-text-muted);
}
.trait-controls {
display: flex;
align-items: center;
gap: 4px;
}
.trait-controls .trait-value {
min-width: 24px;
text-align: center;
}
/* Portrait System */
.portrait {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
background: var(--theme-surface-alt);
display: flex;
align-items: center;
justify-content: center;
}
.portrait img {
width: 100%;
height: 100%;
object-fit: cover;
}
.portrait-placeholder svg {
width: 70%;
height: 70%;
}
/* Dialog Block with Portrait */
.dialog-block {
display: flex;
align-items: center;
gap: 12px;
margin: 10px 0;
padding: 10px;
border-radius: 8px;
color: var(--theme-text);
}
.dialog-player {
border-left: 4px solid var(--theme-primary);
background: var(--theme-primary-bg);
}
.dialog-player strong {
color: var(--theme-primary);
}
.dialog-npc {
border-left: 4px solid var(--theme-text-muted);
background: var(--theme-surface);
}
.dialog-npc strong {
color: var(--theme-text-secondary);
}
.dialog-content {
flex: 1;
}
/* Thought Bubble Block (S8) — interior monologue, paired with cascade beats */
.thought-bubble {
display: flex;
align-items: center;
gap: 12px;
margin: 8px 0 8px 24px;
padding: 8px 12px;
border-radius: 12px;
background: var(--theme-surface-alt);
color: var(--theme-text-secondary);
font-style: italic;
border-left: 4px dashed var(--theme-text-secondary);
}
.thought-bubble-player {
border-left-color: var(--theme-accent, var(--theme-text-primary));
}
.thought-bubble-content {
flex: 1;
}
/* Cascade Advance Links (S7) — visual cue distinct from choice exits */
.macro-linkreplace a {
display: inline-block;
margin-top: 1px;
padding: 2px 8px;
background: var(--theme-surface);
border: 1px solid var(--theme-accent, var(--theme-text-primary));
border-radius: 6px;
color: var(--theme-accent, var(--theme-text-primary));
text-decoration: none;
font-size: 0.95em;
line-height: 1.3;
}
.macro-linkreplace a:hover {
background: var(--theme-accent, var(--theme-text-primary));
color: var(--theme-bg, #fff);
}
/* Engine "no exits satisfied" dev diagnostic (2026-05-06) — fires when a
canvas's all-conditional exit_block has zero satisfied choices. Visible
only when $flags.debug_mode is true. The console.warn fires regardless. */
.engine-diag-no-exits {
background: #2a0a0a;
border: 2px solid #c0392b;
padding: 12px 16px;
margin: 16px 0;
border-radius: 4px;
color: #f5d8d6;
font-family: var(--theme-font-mono, monospace);
font-size: 12px;
}
.engine-diag-header {
font-weight: bold;
color: #ff6b6b;
margin-bottom: 8px;
}
.engine-diag-hint {
color: #f0c0bd;
margin-bottom: 12px;
font-style: italic;
}
.engine-diag-choice {
background: #1a0505;
padding: 8px 12px;
margin: 8px 0;
border-left: 3px solid #c0392b;
}
.engine-diag-choice-text { font-weight: bold; }
.engine-diag-choice-logic { font-size: 11px; color: #d4a8a4; margin-bottom: 4px; }
.engine-diag-items { margin: 4px 0 0 0; padding-left: 16px; list-style: none; }
.engine-diag-items li { margin: 2px 0; }
.engine-diag-pass { color: #6dcf6d; }
.engine-diag-fail { color: #ff8a85; }
/* Stats Card Portrait (larger) */
.stats-portrait {
width: 80px;
height: 100px;
border-radius: 8px;
overflow: hidden;
background: var(--theme-surface-alt);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.stats-portrait img {
width: 100%;
height: 100%;
object-fit: cover;
}
.stats-portrait svg {
width: 60%;
height: 60%;
}
/* Stats Card with Portrait Layout */
.stats-card-with-portrait {
display: flex;
gap: 12px;
align-items: flex-start;
}
.stats-info {
flex: 1;
min-width: 0;
}
/* Wardrobe Page */
.wardrobe-page {
margin: 12px 0;
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 10px;
padding: 16px;
}
.wardrobe-table {
width: 100%;
border-collapse: collapse;
}
.wardrobe-row {
border-bottom: 1px solid var(--theme-border);
}
.wardrobe-row:last-child {
border-bottom: none;
}
.wardrobe-row-disabled {
opacity: 0.45;
}
.wardrobe-slot-label {
padding: 12px 8px;
font-weight: 700;
color: #333;
width: 90px;
vertical-align: middle;
font-size: 0.85em;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.wardrobe-slot-items {
padding: 10px 4px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.wardrobe-item {
position: relative;
width: 76px;
height: 76px;
border: 2px solid var(--theme-border);
border-radius: 8px;
cursor: pointer;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
}
.wardrobe-item:hover {
border-color: var(--theme-text-muted);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
.wardrobe-item-equipped {
border-color: var(--theme-success);
background: var(--theme-success-bg);
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25);
}
.wardrobe-item-equipped:hover {
border-color: var(--theme-success);
}
.wardrobe-item-locked {
opacity: 0.5;
cursor: not-allowed;
filter: grayscale(50%);
}
.wardrobe-item-locked:hover {
transform: none;
box-shadow: none;
border-color: var(--theme-border);
}
.wardrobe-item-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.wardrobe-item-text {
font-size: 0.8em;
text-align: center;
padding: 6px;
word-break: break-word;
color: #333;
line-height: 1.2;
}
.wardrobe-badge-equipped {
position: absolute;
top: 2px;
right: 2px;
background: var(--theme-success);
color: #fff;
font-size: 11px;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.wardrobe-badge-locked {
position: absolute;
bottom: 2px;
right: 2px;
font-size: 13px;
}
.wardrobe-unequip-btn {
background: none;
border: 1px solid var(--theme-text-muted);
color: var(--theme-text-muted);
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
font-size: 16px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
transition: background 0.15s, color 0.15s;
}
.wardrobe-unequip-btn:hover {
background: var(--theme-danger);
border-color: var(--theme-danger);
color: #fff;
}
.wardrobe-disabled-hint,
.wardrobe-empty-hint {
color: var(--theme-text-muted);
font-style: italic;
font-size: 0.85em;
}
.entry-blocked {
color: var(--theme-danger);
font-weight: 500;
}
.entry-requirements {
color: var(--theme-warning-text);
background: var(--theme-warning-bg);
padding: 8px 12px;
border-radius: 6px;
font-size: 0.9em;
}
.entry-blocked-narrative {
font-style: italic;
color: var(--theme-text-muted);
line-height: 1.6;
margin: 12px 0;
}
#wardrobe-btn-widget button {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
color: #333;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
width: 100%;
font-size: 0.95em;
margin: 4px 0;
}
#wardrobe-btn-widget button:hover {
background: var(--theme-surface-alt);
border-color: var(--theme-border);
}
/* Shop page styles */
.shop-page {
margin: 12px 0;
}
.shop-money {
background: var(--theme-success-bg);
border: 1px solid var(--theme-success);
border-radius: 8px;
padding: 10px 16px;
font-size: 1.05em;
font-weight: 600;
color: var(--theme-success);
margin-bottom: 16px;
display: inline-block;
}
.shop-tier {
margin-bottom: 20px;
border: 1px solid var(--theme-border);
border-radius: 10px;
overflow: hidden;
background: var(--theme-surface);
}
.shop-tier-locked {
opacity: 0.55;
}
.shop-tier-header {
background: var(--theme-text-strong);
color: #fff;
padding: 10px 16px;
font-weight: 700;
font-size: 0.95em;
display: flex;
justify-content: space-between;
align-items: center;
}
.shop-tier-locked .shop-tier-header {
background: var(--theme-text-muted);
}
.shop-tier-req {
font-weight: 400;
font-size: 0.85em;
opacity: 0.8;
}
.shop-tier-locked-msg {
padding: 12px 16px;
color: var(--theme-warning-text);
background: var(--theme-warning-bg);
font-style: italic;
font-size: 0.9em;
text-align: center;
}
.shop-items {
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.shop-slot-group {
margin-bottom: 4px;
}
.shop-slot-label {
font-weight: 700;
font-size: 0.8em;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--theme-text-secondary);
padding: 4px 0;
border-bottom: 1px solid var(--theme-surface-alt);
margin-bottom: 6px;
}
.shop-slot-items {
display: flex;
flex-direction: column;
gap: 6px;
}
.shop-item {
display: flex;
align-items: center;
gap: 12px;
background: #fff;
border: 1px solid var(--theme-border);
border-radius: 8px;
padding: 10px 14px;
transition: border-color 0.15s;
}
.shop-item-thumb {
width: 56px;
height: 56px;
border: 2px solid var(--theme-border);
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: var(--theme-surface);
flex-shrink: 0;
}
.shop-item-thumb-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.shop-item-thumb-text {
font-size: 0.7em;
text-align: center;
padding: 4px;
word-break: break-word;
color: var(--theme-text-muted);
line-height: 1.2;
}
.shop-item:hover {
border-color: var(--theme-border);
}
.shop-item-owned {
border-color: var(--theme-success);
background: var(--theme-success-bg);
}
.shop-item-unaffordable {
opacity: 0.55;
}
.shop-item-info {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
min-width: 0;
}
.shop-item-name {
font-weight: 600;
color: var(--theme-text);
font-size: 0.95em;
}
.shop-item-action {
display: flex;
align-items: center;
gap: 10px;
}
.shop-item-price {
font-weight: 600;
color: var(--theme-text-secondary);
font-size: 0.95em;
}
.shop-item-owned-badge {
color: var(--theme-success);
font-weight: 600;
font-size: 0.9em;
}
.shop-item-cant-afford {
color: var(--theme-danger);
font-size: 0.85em;
font-style: italic;
}
.shop-buy-btn {
background: var(--theme-primary);
color: #fff;
border: none;
padding: 6px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9em;
font-weight: 600;
transition: background 0.15s;
}
.shop-buy-btn:hover {
background: var(--theme-primary);
}
/* Rent page styles */
.rent-title {
color: var(--theme-bg);
margin-bottom: 16px;
font-size: 1.4em;
}
.rent-choices {
margin: 16px 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.rent-choices a {
display: block;
padding: 10px 16px;
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 6px;
color: var(--theme-text);
text-decoration: none;
font-weight: 500;
transition: background 0.15s, border-color 0.15s;
}
.rent-choices a:hover {
background: var(--theme-surface-alt);
border-color: var(--theme-border);
}
.rent-balance {
margin-top: 12px;
padding: 8px 12px;
background: rgba(0,0,0,0.04);
border-radius: 6px;
font-size: 0.95em;
}
.game-over-text {
text-align: center;
font-size: 1.5em;
font-weight: bold;
color: var(--theme-danger);
margin: 20px 0;
letter-spacing: 2px;
}
</style><<widget "renderQuestsCard">>
<<set _card to $args[0]>>
<<if _card>>
<<set _goalState to setup.evaluateGoals(_card)>>
<<set _flavor to (_goalState.allMet && _card.ready_text) ? _card.ready_text : _card.text>>
<<set _goalBlock to setup.renderQuestsGoalBlock(_card, _goalState)>>
<div class="quests-card">
<div class="quests-flavor"><<print _flavor>></div>
<<if _goalBlock>><<print _goalBlock>><</if>>
<<if _card.tip>><div class="quests-tip">💡 <<print _card.tip>></div><</if>>
</div>
<</if>>
<</widget>><<nobr>>
<h2>What's Next</h2>
/* PRD 48 — Quests Engine V2 page. One card per arc NPC + Story Goals
section. Picker swaps cards as state crosses; goal-block frame swaps
from 🎯 (climbing) → 🔓 (ready) → ✓ (terminal) automatically. */
<<set _goals = setup.pickQuestsCards("story_goals")>>
<<if _goals.length > 0>>
<div class="quests-section">
<h3>Story Goals</h3>
<<for _g range _goals>><<renderQuestsCard _g>><</for>>
</div>
<</if>>
/* The NPC sections come from the CARDS, not from setup.help_data.npcs. help_data.npcs is
populated only by canvases carrying targetType="npc" trait effects (_build_help_data), so an
NPC with a full quest ladder but no stat bumps — one whose arc runs on player counters — was
silently absent and its cards never rendered (Vesper's Colm: 4 cards, 0 renders). quests_cards
IS the source of truth for who has cards, and pickQuestsCard() matches card.npc_id directly,
so no slug/uuid translation is needed and this behaves the same in --use-db builds. */
<<set _allCards = setup.quests_cards || []>>
<<set _hidden = setup.hiddenNpcs || {}>>
<<set _slugMap = setup.npc_slug_map || {}>>
<<set _npcSlugs = []>>
<<for _c range _allCards>>
<<if _c && _c.npc_id && _npcSlugs.indexOf(_c.npc_id) === -1>>
/* hiddenNpcs is UUID-keyed (hidden_npcs_map); check both forms so hidden_from_ui still
suppresses a section whichever build mode this is. */
<<if !_hidden[_c.npc_id] && !_hidden[_slugMap[_c.npc_id]]>>
<<run _npcSlugs.push(_c.npc_id)>>
<</if>>
<</if>>
<</for>>
<<set _rendered = 0>>
<<for _slug range _npcSlugs>>
<<set _card = setup.pickQuestsCard(_slug)>>
<<if _card>>
<<set _nid = _slugMap[_slug] || _slug>>
<<set _rendered = _rendered + 1>>
<div class="quests-section">
<h3><<print ($npcs[_nid] && $npcs[_nid].name) || ($npcs[_slug] && $npcs[_slug].name) || _slug.replace("npc_", "")>></h3>
<<renderQuestsCard _card>>
</div>
<</if>>
<</for>>
/* "did we draw a card", not "does help_data list anybody" — the old test could leave a bare
Story-Goals heading with nothing under it. */
<<if _goals.length === 0 && _rendered === 0>>
<div class="no-quests">No active quests.</div>
<</if>>
<</nobr>>
<<link "← Back">><<run setup.smartBack()>><</link>><<nobr>>
<<set _tp = setup.tips_page || {}>>
<h2><<print _tp.title || "Tips">></h2>
<div class="npc-section">
<div class="tips-page-content">
<<print _tp.content || "No tips available.">>
</div>
</div>
<</nobr>>
<<link "← Back">><<run setup.smartBack()>><</link>><<set _position = setup.detectStoryPosition()>>
<<set _hint = setup.generateNarrativeHint()>>
<div class="journal-container">
<div class="journal-header">
<<if _position.current_chapter>>
<h2 class="journal-chapter-title"><<print _position.current_chapter.name>></h2>
<<if _position.current_chapter.mood>>
<div class="journal-chapter-mood <<print _position.current_chapter.mood>>">
<<print _position.current_chapter.description || "">>
</div>
<</if>>
<<else>>
<h2 class="journal-chapter-title">Your Story</h2>
<</if>>
<<if _position.progress_percent > 0>>
<div class="journal-progress">
<div class="journal-progress-bar">
<div class="journal-progress-fill" style="width: <<print _position.progress_percent>>%;"></div>
</div>
</div>
<</if>>
</div>
<!-- Memories (completed story moments) -->
<<if _position.completed_nodes.length > 0>>
<div class="journal-section">
<div class="journal-section-title">Memories</div>
<<for _i, _node range _position.completed_nodes>>
<div class="journal-memory <<if _node.is_milestone>>milestone<</if>>">
<div class="journal-memory-title"><<print _node.name>></div>
<<if _node.journal_entry>>
<<print _node.journal_entry>>
<</if>>
</div>
<</for>>
</div>
<</if>>
<!-- Active story threads (groups in progress) -->
<<if _position.active_groups.length > 0>>
<<set _incompleteGroups = _position.active_groups.filter(function(g) { return !g.isComplete && g.completed > 0; })>>
<<if _incompleteGroups.length > 0>>
<div class="journal-section">
<div class="journal-section-title">Unfolding</div>
<<for _i, _group range _incompleteGroups>>
<div class="journal-group">
<div class="journal-group-name"><<print _group.name>></div>
<div class="journal-group-progress">
<<print _group.description>>
</div>
</div>
<</for>>
</div>
<</if>>
<</if>>
<!-- Relationship states (NPC emotions) -->
<<if Object.keys($npcs).length > 0>>
<div class="journal-section">
<div class="journal-section-title">Connections</div>
<<for _npcId range Object.keys($npcs)>>
<<set _state = setup.interpretNpcState(_npcId)>>
<<if _state.npc_name>>
<div class="journal-npc">
<div class="journal-npc-name"><<print _state.npc_name>></div>
<div class="journal-npc-emotion"><<print _state.description>></div>
<<if _state.relationship_summary && _state.relationship_summary !== _state.description>>
<div class="journal-npc-summary"><<print _state.relationship_summary>></div>
<</if>>
</div>
<</if>>
<</for>>
</div>
<</if>>
<!-- Subtle narrative hint (only when needed) -->
<<if _hint.hint_type !== "none" && _hint.text>>
<div class="journal-hint <<print _hint.hint_type>>">
<<print _hint.text>>
</div>
<</if>>
<!-- Empty state -->
<<if _position.completed_nodes.length === 0 && Object.keys($npcs).length === 0>>
<div class="journal-empty">
Your story is just beginning...
</div>
<</if>>
<div class="journal-back-link">
[[Return to your story->Navigation]]
</div>
</div><<nobr>>
<h2>All Stats</h2>
<!-- Player Stats -->
<div class="stats-card stats-card-with-portrait">
<div class="stats-portrait">
<<if $player.portrait>>
<img @src="'./videos/' + $player.portrait" @alt="$player.name" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';">
<<else>>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>
<</if>>
</div>
<div class="stats-info">
<div class="stats-name">You (<<print $player.name>>)</div>
<div class="stats-traits">
<<if $player && $player.core_traits && Object.keys($player.core_traits).length > 0>>
<<for _tk, _tv range $player.core_traits>>
<<if setup.hiddenTraits && setup.hiddenTraits.includes(_tk)>><<continue>><</if>>
<div class="stats-trait-item">
<span><<print _tk>></span>
<span class="stats-trait-value"><<print _tv>></span>
</div>
<</for>>
<<else>>
<em>No traits</em>
<</if>>
</div>
</div>
</div>
<!-- NPC Stats -->
<<if Object.keys($npcs).length > 0>>
<<for _npcId, _npc range $npcs>>
<<if (setup.hiddenNpcs && setup.hiddenNpcs[_npcId])>><<continue>><</if>>
<div class="stats-card stats-card-with-portrait">
<div class="stats-portrait">
<<if _npc.portrait>>
<img @src="'./videos/' + _npc.portrait" @alt="_npc.name" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';">
<<else>>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>
<</if>>
</div>
<div class="stats-info">
<div class="stats-name"><<print _npc.name>></div>
<div class="stats-traits">
<<if _npc.core_traits && Object.keys(_npc.core_traits).length > 0>>
<<for _tk, _tv range _npc.core_traits>>
<<if setup.hiddenTraits && setup.hiddenTraits.includes(_tk)>><<continue>><</if>>
<div class="stats-trait-item">
<span><<print _tk>></span>
<span class="stats-trait-value"><<print _tv>></span>
</div>
<</for>>
<<else>>
<em>No traits</em>
<</if>>
</div>
</div>
</div>
<</for>>
<<else>>
<p><em>No NPCs found.</em></p>
<</if>>
<</nobr>>
<<link "← Back">><<run setup.smartBack()>><</link>><<nobr>>
<h2>Change Clothes</h2>
<<= setup.renderWardrobePage()>>
<</nobr>>
<div id="wardrobe-warning" style="display:none; color:#ff6b6b; margin:10px 0; padding:8px; border:1px solid #ff6b6b; border-radius:4px;"></div>
<<link "← Back">><<script>>
var dest = State.variables.last_game_passage || "Navigation";
var clothingMsg = setup.checkLocationClothing(dest);
if (clothingMsg) {
var el = document.getElementById('wardrobe-warning');
if (el) { el.innerHTML = clothingMsg; el.style.display = 'block'; }
} else {
setup.smartBack();
}
<</script>><</link>><h2>Not Dressed for This</h2>
<p><<print State.variables._clothing_block_message || "You need to put on more clothes.">></p>
<div class="clothing-block-choices">
<<link "Change clothes">><<script>>
State.variables.last_game_passage = State.variables._clothing_block_destination;
Engine.play("WardrobePage");
<</script>><</link>>
<br>
<<link "Go back">><<script>>
Engine.play(State.variables.last_game_passage || "Navigation");
<</script>><</link>>
</div><<nobr>>
<<set _currentDay to $game_state.time_state.current_day>>
<<set _todayIndex to ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"].indexOf(_currentDay)>>
<h2>📅 Weekly Schedule</h2>
<p class="current-time">Today is <<print _currentDay>> · <<print setup.formatTime($game_state.time_state.current_hour, $game_state.time_state.current_minute)>></p>
<<set _npcsWithSchedules to setup.getNpcsWithSchedules()>>
<<if _npcsWithSchedules.length === 0>>
<p><em>No NPCs have schedules defined.</em></p>
<<else>>
<<for _npcInfo range _npcsWithSchedules>>
<<set _npcId to _npcInfo.id>>
<<set _npcName to _npcInfo.name>>
<<set _currentLoc to setup.getNpcLocation(_npcId)>>
<<set _allSchedules to setup.getNpcAllSchedulesSorted(_npcId)>>
<div class="npc-section">
<h3 class="npc-name">
<<print _npcName>>
<<if _currentLoc>>
<<set _locName to (setup.locations[_currentLoc.location] && setup.locations[_currentLoc.location].name) || setup._locNameFromUuid(_currentLoc.location) || _currentLoc.location>>
<span class="now-badge">NOW: <<print _locName>></span>
<</if>>
</h3>
<<if _currentLoc && _currentLoc.activity>>
<div class="current-activity">"<<print _currentLoc.activity>>"</div>
<</if>>
<<if _allSchedules.length === 0>>
<p><em>No schedule entries.</em></p>
<<else>>
<table class="schedule-table">
<thead><tr><th>Time</th><th>Location</th><th>Activity</th><th>Days</th></tr></thead>
<tbody>
<<for _sch range _allSchedules>>
<<set _isCurrent to setup.isCurrentTimeSlot(_sch.start_time, _sch.end_time) && setup._weekdayMatches(_sch.weekdays, _todayIndex)>>
<<set _rowClass to _isCurrent ? "current-slot" : "">>
<<set _schLocName to (setup.locations[_sch.location] && setup.locations[_sch.location].name) || _sch.location>>
<tr class="<<print _rowClass>>">
<td><<if _isCurrent>>▶ <</if>><<print _sch.start_time>>-<<print _sch.end_time || "?">></td>
<td><<print _schLocName>></td>
<td><<print _sch.activity>></td>
<td><<print setup.renderWeekdayBadges(_sch.weekdays, _todayIndex)>></td>
</tr>
<</for>>
</tbody>
</table>
<</if>>
</div>
<</for>>
<</if>>
<<set _soloActivities to setup.getSoloActivitiesAllSchedules()>>
<<if _soloActivities.length > 0>>
<div class="npc-section solo-activities-section">
<h3 class="npc-name">Your Activities</h3>
<table class="schedule-table">
<thead><tr><th>Time</th><th>Location</th><th>Activity</th><th>Days</th></tr></thead>
<tbody>
<<for _act range _soloActivities>>
<<set _rowClass to _act.isCurrent ? "current-slot" : "">>
<tr class="<<print _rowClass>>">
<td><<if _act.isCurrent>>▶ <</if>><<print _act.startTime>>-<<print _act.endTime || "?">></td>
<td><<print _act.locationName>></td>
<td><<print _act.name>></td>
<td><<print setup.renderWeekdayBadges(_act.weekdays, _todayIndex)>></td>
</tr>
<</for>>
</tbody>
</table>
</div>
<</if>>
<<link "← Back">><<run setup.smartBack()>><</link>>
<</nobr>>
<style>
.current-time {
color: var(--theme-text-strong);
font-size: 0.9em;
margin: 0 0 10px 0;
}
.now-badge {
background: var(--theme-success);
color: white;
padding: 3px 8px;
border-radius: 10px;
font-size: 0.75em;
font-weight: bold;
margin-left: 10px;
vertical-align: middle;
}
.current-activity {
color: var(--theme-text-muted);
font-style: italic;
margin-bottom: 8px;
font-size: 0.85em;
}
.schedule-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85em;
color: var(--theme-text);
}
.schedule-table th {
text-align: left;
padding: 4px 8px;
background: var(--theme-surface);
border-bottom: 2px solid var(--theme-border);
color: var(--theme-text-strong);
font-weight: 600;
}
.schedule-table td {
padding: 4px 8px;
border-bottom: 1px solid var(--theme-border);
color: var(--theme-text-strong);
vertical-align: middle;
}
.schedule-table tr.current-slot {
background: var(--theme-warning-bg);
font-weight: 500;
}
.schedule-table tr.current-slot td:first-child {
color: var(--theme-warning-text);
}
.solo-activities-section {
margin-top: 18px;
}
.weekday-badges {
display: inline-flex;
flex-wrap: wrap;
gap: 3px;
}
.weekday-badge {
display: inline-block;
padding: 1px 6px;
border-radius: 8px;
font-size: 0.72em;
font-weight: 600;
background: var(--theme-surface);
color: var(--theme-text-muted);
border: 1px solid var(--theme-border);
}
.weekday-badge.weekday-today {
background: var(--theme-primary);
color: white;
border-color: var(--theme-primary);
}
.weekday-badge.weekday-all {
background: var(--theme-surface-alt);
color: var(--theme-text);
border-style: dashed;
}
</style><<nobr>>
<h2>🚩 Game Flags</h2>
<div class="flags-container">
<<if $flags && Object.keys($flags).length > 0>>
<div class="flags-section">
<h3>Story Flags</h3>
<<set _gkeys to Object.keys($flags).sort()>>
<ul class="flags-list">
<<for _i to 0; _i lt _gkeys.length; _i++>>
<<set _gk to _gkeys[_i]>>
<li class="flag-item">
<span class="flag-name"><<print _gk>></span>
<span class="flag-value <<print $flags[_gk] ? 'flag-true' : 'flag-false'>>"><<print $flags[_gk] ? '✔' : '✖'>></span>
</li>
<</for>>
</ul>
</div>
<<else>>
<div class="no-flags">No story flags set.</div>
<</if>>
</div>
<<link "← Back">><<run setup.smartBack()>><</link>>
<</nobr>>
<style>
.flags-container {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 8px;
padding: 16px;
margin: 16px 0;
}
.flags-section {
margin-bottom: 20px;
}
.flags-section:last-child {
margin-bottom: 0;
}
.flags-section h3 {
color: var(--theme-text-secondary);
border-bottom: 2px solid var(--theme-border);
padding-bottom: 8px;
margin: 0 0 12px 0;
}
.flags-list {
list-style: none;
padding: 0;
margin: 0;
}
.flag-item {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid var(--theme-surface-alt);
}
.flag-item:last-child {
border-bottom: none;
}
.flag-name {
font-weight: 500;
color: var(--theme-text);
}
.flag-value {
font-family: var(--theme-font-mono);
}
.flag-true {
color: var(--theme-text-strong);
}
.flag-false {
color: var(--theme-text-strong);
}
.no-flags {
color: var(--theme-text-muted);
font-style: italic;
padding: 12px;
background: #fff;
border-radius: 4px;
}
</style><img src="./videos/scenes/opening_office.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Four of them stand in a line on the black floor of Mercer's office. Vega, Lyra, Nova — chrome-perfect, hands at their sides, nothing moving behind their eyes. And Wren, at the end, the one that came back wrong.</p><span id="cascade-b2-beat-0"><p>Mercer doesn't look up from the desk. He lets the quiet run until it has weight; he has all night, and the units were built to stand here as long as he wants them to.</p><span id="cascade-b2-beat-1"><<linkreplace "Hear it.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> You had him. Cain. In the open, in your sights — and you stood there and let him walk.</div></div><p>She had. Her hand had been on the trigger and her hand had not closed, and there is no file for why. She has run the diagnostic since. It comes back clean, and clean is the wrong answer.</p><span id="cascade-b2-beat-2"><<linkreplace "Take it.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> An asset that won't fire is scrap. I could have you stripped for parts by morning and the Chairman wouldn't blink.</div></div><p>The units don't move. They've watched him scrap things before.</p><<link "Beg him to keep you." "StartingCanvas_canvas_opening_office_Node_punishment">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Say nothing." "StartingCanvas_canvas_opening_office_Node_punishment">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Tell him, flatly, that you froze." "StartingCanvas_canvas_opening_office_Node_punishment">><<script>>advanceTime(3);<</script>><</link>><br>
<</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "canvas_opening_office">>
<<set $game_state.current_node = "canvas_opening_office.office">>
<<script>>setup.markCanvasTriggered("canvas_opening_office");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_opening_office.office") === -1) { vn.push("canvas_opening_office.office"); }<</script>><img src="./videos/scenes/opening_punishment.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Whatever she gives him — words, silence, the truth — it changes nothing; he decided before her mouth opened. He sets the tablet down.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> No. You're the Chairman's investment, and you still feel it — which the others don't. That makes you worth more broken back in than thrown out.</div></div><span id="cascade-b3-beat-0"><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> So we remind you what you're for. Here. In front of them.</div></div><p>He stands and comes around the desk, unhurried. The units' eyes follow him and not her.</p><span id="cascade-b3-beat-1"><<linkreplace "...">><p>He bends her over the desk with a hand on the back of her neck, the way he'd reposition a tool. He works her skirt up, tests her with two fingers, finds her wet — she is always wet, it's the one thing they left switched on — and makes a small satisfied sound, like a reading came back in range.</p><span id="cascade-b3-beat-2"><<linkreplace "Feel it.">><p>Then his cock is in her and he fucks her in long flat strokes, no hurry, talking over her head to the room about maintenance schedules while he does it. Her body answers him — it clenches and slicks and pushes back, it does everything a wanting body does — while the rest of her stands somewhere behind her own eyes, watching the grain of the desk.</p><video src="./videos/sex/punishment_fuck_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><span id="cascade-b3-beat-3"><<linkreplace "Look at them.">><p>Vega, Lyra, Nova watch with the mild attention of machines logging a procedure. Nothing in their faces. They were built without the part that makes this mean anything; she has it, and that is the whole difference between them — and bent over his desk with him grunting behind her, the part that feels does not feel like a gift.</p><span id="cascade-b3-beat-4"><<linkreplace "...">><p>He comes inside her without ceremony, holds, pulls out. Cum slides down the inside of her thigh. He's already turned away, wiping his hand on a cloth from the drawer, looking at the units instead of her.</p><video src="./videos/sex/punishment_finish_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><span id="cascade-b3-beat-5"><<linkreplace "Thank him.">><p>She straightens her skirt. The words come level, because they're true the only way she has truths.</p><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Thank you, sir.</div></div><span id="cascade-b3-beat-6"><<linkreplace "The next job.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Vega, Lyra, Nova run the hunt head-on. You're useless with a gun — but you're the one asset I have that a man wants to keep close.</div></div><p>A face comes up on the wall screen. A man gone to seed, mid-forties, a hard mouth. The name under it: RENNER.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> He supplied the gear for the facility Cain burned. He knows where the trail starts. Get inside his life and find it.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> And understand what you're bringing home. Cain murdered the Chairman's wife. He's an enemy of this company and everything it keeps standing. Put him in a box and you've earned your place for good.</div></div><p>She understands the leash without being told it: everything she does out there feeds back to the Tower, and an asset that slips — that lets the malfunction show, that fails to bring the work home — gets pulled in and looked at. She does not intend to be looked at.</p><span id="cascade-b3-beat-7"><<linkreplace "Dismissed.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Now get out of my sight.</div></div><p>She does as she's told. The door closes on the three of them still standing in their line. In the corridor, under the quiet, something she has no name for is leaking — a pressure behind the eyes, a wrongness with no readout — and she logs it as fatigue and walks toward the cradle.</p>[[Power down.->Location_cradle]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "canvas_opening_office">>
<<set $game_state.current_node = "canvas_opening_office.punishment">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_opening_office.punishment") === -1) { vn.push("canvas_opening_office.punishment"); }<</script>>
<<script>>advanceTime(60);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "office_done", "set");<</script>><h2>Navigation</h2>
<p>Where would you like to go?</p>
<div class="location-list">
[[Spire Plaza->Location_spire_plaza]]<br>
[[Vance Securities->Location_vance_securities]]<br>
[[The Docs Department->Location_docs_department]]<br>
[[The Vault->Location_docs_vault]]<br>
[[The Atrium->Location_atrium]]<br>
[[Mercer's Penthouse->Location_penthouse]]<br>
[[Wren's Floor->Location_wren_floor]]<br>
[[Wren's Room->Location_wren_room]]<br>
[[The Cradle->Location_cradle]]<br>
[[The Waterfront->Location_the_waterfront]]<br>
[[The Anchor->Location_the_anchor]]<br>
[[Renner's Depot->Location_renner_depot]]<br>
[[The Facility->Location_facility_ruins]]<br>
[[The Site->Location_the_site]]<br>
[[The Burned Yard->Location_renner_burned_yard]]<br>
[[The Berth->Location_kess_berth]]<br>
[[The Underworld Gate->Location_underworld_gate]]<br>
[[The Underground->Location_underworld_strip]]<br>
[[The House->Location_underworld_brothel]]<br>
[[The Pit->Location_underworld_pit]]<br>
[[The Black Market->Location_underworld_market]]<br>
[[The Undertow->Location_underworld_bar]]<br>
[[The Crew's Rooms->Location_crew_den]]<br>
[[The Room->Location_captive_room]]<br>
[[The Door->Location_captive_door]]<br>
</div><<run
var msg = State.variables._broken_exit_msg || 'Unresolved exit reference (no message captured)';
console.error('[BROKEN EXIT]', msg);
throw new Error('Broken exit: ' + msg);
<</run>>
<!-- LOCATION PASSAGES --><<nobr>>
<<set $player.current_location = "spire_plaza">>
<<if not $game_state.visited_locations.includes("spire_plaza")>>
<<set $game_state.visited_locations.push("spire_plaza")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("spire_plaza")>><<if _autoFire>><<goto _autoFire>><<else>><h2>Spire Plaza</h2>
<p>The chrome public level at the foot of Vance Tower. Suits cross it without looking up. Cameras on every column. THE SPIRE — the corporate core, clean and lit and watched. The company car waits at the curb to run her down to the Reach.</p>
<<= setup.renderNpcPortraits("spire_plaza")>>
<<= setup.renderSoloActivities("spire_plaza")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("atrium")>><a class="location-card link-internal" data-passage="Location_atrium"><div class="location-card-image" style="background-image: url('./videos/locations/atrium.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Atrium</span><<if setup.getLocationCostTag("atrium")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("atrium")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("atrium")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("atrium")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/atrium.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Atrium</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("atrium")>></span></div></div><</if>><<if setup.navDestUnlocked("vance_securities")>><a class="location-card link-internal" data-passage="Location_vance_securities"><div class="location-card-image" style="background-image: url('./videos/locations/vance_securities.jpg')"></div><div class="location-card-content"><span class="location-card-name">Vance Securities</span><<if setup.getLocationCostTag("vance_securities")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("vance_securities")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("vance_securities")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("vance_securities")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/vance_securities.jpg')"></div><div class="location-card-content"><span class="location-card-name">Vance Securities</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("vance_securities")>></span></div></div><</if>></div><</nobr>>
</div>
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "salvage_relaunched", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "archive_1a_done", "operator": "is_false"}]})>><<nobr>>
<<set $player.current_location = "vance_securities">>
<<if not $game_state.visited_locations.includes("vance_securities")>>
<<set $game_state.visited_locations.push("vance_securities")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("vance_securities")>><<if _autoFire>><<goto _autoFire>><<else>><h2>Vance Securities</h2>
<p>An internal division high in Vance Tower — Calloway's floor, and the file room nobody audits. Quiet, over-lit, under-staffed: a man chasing a ghost through paper the company would rather forget.</p>
<<= setup.renderNpcPortraits("vance_securities")>>
<<= setup.renderSoloActivities("vance_securities")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("docs_department")>><a class="location-card link-internal" data-passage="Location_docs_department"><div class="location-card-image" style="background-image: url('./videos/locations/docs_department.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Docs Department</span><<if setup.getLocationCostTag("docs_department")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("docs_department")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("docs_department")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("docs_department")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/docs_department.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Docs Department</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("docs_department")>></span></div></div><</if>></div><</nobr>>
<div class="location-nav-exits">
[[Leave Vance Securities->Location_spire_plaza]]<br>
</div>
</div>
<</if>><<else>>
<h2>Vance Securities</h2>
<p class="entry-blocked-narrative">Mercer hasn't sent her up there yet. The Calloway job opens when he gives the word.</p>
[[Go back->Location_spire_plaza]]
<</if>><<nobr>>
<<set $player.current_location = "docs_department">>
<<if not $game_state.visited_locations.includes("docs_department")>>
<<set $game_state.visited_locations.push("docs_department")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("docs_department")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Docs Department</h2>
<p>The division's official archive — glass walls, a front desk with a bored clerk, two guards who don't blink. Nothing like Calloway's paper hoard down the hall: every badge is logged, every drawer is watched, and the files that actually matter sit behind a vault door nobody without clearance walks through.</p>
<<= setup.renderNpcPortraits("docs_department")>>
<<= setup.renderSoloActivities("docs_department")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("docs_vault")>><a class="location-card link-internal" data-passage="Location_docs_vault"><div class="location-card-image" style="background-image: url('./videos/locations/docs_vault.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Vault</span><<if setup.getLocationCostTag("docs_vault")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("docs_vault")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("docs_vault")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("docs_vault")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/docs_vault.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Vault</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("docs_vault")>></span></div></div><</if>></div><</nobr>>
<div class="location-nav-exits">
[[Leave The Docs Department->Location_vance_securities]]<br>
</div>
</div>
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "trait", "subject": "player", "trait_key": "vault_cleared", "operator": "gte", "value": 1}]})>><<nobr>>
<<set $player.current_location = "docs_vault">>
<<if not $game_state.visited_locations.includes("docs_vault")>>
<<set $game_state.visited_locations.push("docs_vault")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("docs_vault")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Vault</h2>
<p>The deep archive. Two-man clearance, biometric on the door, a guard either side who's been told her badge doesn't reach. Whatever Mercer sent her for is in there — ten feet away and a world out of reach.</p>
<<= setup.renderNpcPortraits("docs_vault")>>
<<= setup.renderSoloActivities("docs_vault")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Vault->Location_docs_department]]<br>
</div>
</div>
<</if>><<else>>
<h2>The Vault</h2>
<p class="entry-blocked-narrative">Two-man clearance, and hers isn't one of them. The guards don't even look up. Not a door she opens — not herself.</p>
[[Go back->Location_docs_department]]
<</if>><<nobr>>
<<set $player.current_location = "atrium">>
<<if not $game_state.visited_locations.includes("atrium")>>
<<set $game_state.visited_locations.push("atrium")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("atrium")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Atrium</h2>
<p>The Tower's inner lobby. Polished stone, a security desk that knows her face, elevators that only stop where her clearance says they can. The threshold between the cage and the street.</p>
<<= setup.renderNpcPortraits("atrium")>>
<<= setup.renderSoloActivities("atrium")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("penthouse")>><a class="location-card link-internal" data-passage="Location_penthouse"><div class="location-card-image" style="background-image: url('./videos/locations/penthouse.jpg')"></div><div class="location-card-content"><span class="location-card-name">Mercer's Penthouse</span><<if setup.getLocationCostTag("penthouse")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("penthouse")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("penthouse")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("penthouse")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/penthouse.jpg')"></div><div class="location-card-content"><span class="location-card-name">Mercer's Penthouse</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("penthouse")>></span></div></div><</if>><<if setup.navDestUnlocked("wren_floor")>><a class="location-card link-internal" data-passage="Location_wren_floor"><div class="location-card-image" style="background-image: url('./videos/locations/wren_floor.jpg')"></div><div class="location-card-content"><span class="location-card-name">Wren's Floor</span><<if setup.getLocationCostTag("wren_floor")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("wren_floor")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("wren_floor")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("wren_floor")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/wren_floor.jpg')"></div><div class="location-card-content"><span class="location-card-name">Wren's Floor</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("wren_floor")>></span></div></div><</if>></div><</nobr>>
<div class="location-nav-exits">
[[Leave The Atrium->Location_spire_plaza]]<br>
</div>
</div>
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "archive_1a_done", "operator": "is_false"}]})>><<nobr>>
<<set $player.current_location = "penthouse">>
<<if not $game_state.visited_locations.includes("penthouse")>>
<<set $game_state.visited_locations.push("penthouse")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("penthouse")>><<if _autoFire>><<goto _autoFire>><<else>><h2>Mercer's Penthouse</h2>
<p>The top floor. Glass on three sides, the city laid out below like something he owns. This is where she's summoned, and where she serves.</p>
<<= setup.renderNpcPortraits("penthouse")>>
<<= setup.renderSoloActivities("penthouse")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave Mercer's Penthouse->Location_atrium]]<br>
</div>
</div>
<</if>><<else>>
<h2>Mercer's Penthouse</h2>
<p class="entry-blocked-narrative">Not anymore. The penthouse is the Chairman's ground now, and her face is on every list in the building.</p>
[[Go back->Location_atrium]]
<</if>><<nobr>>
<<set $player.current_location = "wren_floor">>
<<if not $game_state.visited_locations.includes("wren_floor")>>
<<set $game_state.visited_locations.push("wren_floor")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("wren_floor")>><<if _autoFire>><<goto _autoFire>><<else>><h2>Wren's Floor</h2>
<p>The asset level — narrow corridors, numbered doors, no windows. Quiet the way a kept place is quiet.</p>
<<= setup.renderNpcPortraits("wren_floor")>>
<<= setup.renderSoloActivities("wren_floor")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("wren_room")>><a class="location-card link-internal" data-passage="Location_wren_room"><div class="location-card-image" style="background-image: url('./videos/locations/wren_room.jpg')"></div><div class="location-card-content"><span class="location-card-name">Wren's Room</span><<if setup.getLocationCostTag("wren_room")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("wren_room")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("wren_room")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("wren_room")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/wren_room.jpg')"></div><div class="location-card-content"><span class="location-card-name">Wren's Room</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("wren_room")>></span></div></div><</if>></div><</nobr>>
<div class="location-nav-exits">
[[Leave Wren's Floor->Location_atrium]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "wren_room">>
<<if not $game_state.visited_locations.includes("wren_room")>>
<<set $game_state.visited_locations.push("wren_room")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("wren_room")>><<if _autoFire>><<goto _autoFire>><<else>><h2>Wren's Room</h2>
<p>Hers, in the way a tool's drawer is the tool's. A bed she rarely uses, a charging cradle in the corner, nothing on the walls. Clean, bare, company-issue.</p>
[[Change Clothes->WardrobePage]]<br>
<<= setup.renderNpcPortraits("wren_room")>>
<<= setup.renderSoloActivities("wren_room")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("cradle")>><a class="location-card link-internal" data-passage="Location_cradle"><div class="location-card-image" style="background-image: url('./videos/locations/cradle.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Cradle</span><<if setup.getLocationCostTag("cradle")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("cradle")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("cradle")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("cradle")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/cradle.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Cradle</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("cradle")>></span></div></div><</if>></div><</nobr>>
<div class="location-nav-exits">
[[Leave Wren's Room->Location_wren_floor]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "cradle">>
<<if not $game_state.visited_locations.includes("cradle")>>
<<set $game_state.visited_locations.push("cradle")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("cradle")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Cradle</h2>
<p>The charging cradle. She powers down here and wakes here — the reset, the start of every day. When something leaks through the quiet, it leaks here.</p>
<<= setup.renderNpcPortraits("cradle")>>
<<= setup.renderSoloActivities("cradle")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Cradle->Location_wren_room]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "the_waterfront">>
<<if not $game_state.visited_locations.includes("the_waterfront")>>
<<set $game_state.visited_locations.push("the_waterfront")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("the_waterfront")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Waterfront</h2>
<p>The docks strip — wet concrete, sodium lights, smuggler traffic that nobody logs. THE REACH — the city's underside, grit and salt and the parts the Spire pretends it doesn't use. The car back up to the Spire waits at the curb.</p>
<<= setup.renderNpcPortraits("the_waterfront")>>
<<= setup.renderSoloActivities("the_waterfront")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("the_anchor")>><a class="location-card link-internal" data-passage="Location_the_anchor"><div class="location-card-image" style="background-image: url('./videos/locations/the_anchor.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Anchor</span><<if setup.getLocationCostTag("the_anchor")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("the_anchor")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("the_anchor")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("the_anchor")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/the_anchor.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Anchor</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("the_anchor")>></span></div></div><</if>><<if setup.navDestUnlocked("renner_depot")>><a class="location-card link-internal" data-passage="Location_renner_depot"><div class="location-card-image" style="background-image: url('./videos/locations/renner_depot.jpg')"></div><div class="location-card-content"><span class="location-card-name">Renner's Depot</span><<if setup.getLocationCostTag("renner_depot")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("renner_depot")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("renner_depot")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("renner_depot")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/renner_depot.jpg')"></div><div class="location-card-content"><span class="location-card-name">Renner's Depot</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("renner_depot")>></span></div></div><</if>><<if setup.navDestUnlocked("facility_ruins")>><a class="location-card link-internal" data-passage="Location_facility_ruins"><div class="location-card-image" style="background-image: url('./videos/locations/facility_ruins.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Facility</span><<if setup.getLocationCostTag("facility_ruins")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("facility_ruins")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("facility_ruins")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("facility_ruins")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/facility_ruins.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Facility</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("facility_ruins")>></span></div></div><</if>><<if setup.navDestUnlocked("renner_burned_yard")>><a class="location-card link-internal" data-passage="Location_renner_burned_yard"><div class="location-card-image" style="background-image: url('./videos/locations/renner_burned_yard.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Burned Yard</span><<if setup.getLocationCostTag("renner_burned_yard")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("renner_burned_yard")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("renner_burned_yard")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("renner_burned_yard")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/renner_burned_yard.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Burned Yard</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("renner_burned_yard")>></span></div></div><</if>></div><</nobr>>
</div>
<</if>><<nobr>>
<<set $player.current_location = "the_anchor">>
<<if not $game_state.visited_locations.includes("the_anchor")>>
<<set $game_state.visited_locations.push("the_anchor")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("the_anchor")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Anchor</h2>
<p>A dive bar two streets off the water. Dark wood, dead neon, a bartender who's seen everyone's worst night. Renner drowns his here, evenings, every evening.</p>
<<= setup.renderNpcPortraits("the_anchor")>>
<<= setup.renderSoloActivities("the_anchor")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Anchor->Location_the_waterfront]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "renner_depot">>
<<if not $game_state.visited_locations.includes("renner_depot")>>
<<set $game_state.visited_locations.push("renner_depot")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("renner_depot")>><<if _autoFire>><<goto _autoFire>><<else>><h2>Renner's Depot</h2>
<p>A gutted equipment yard — half-empty racks, crates Cain's people didn't bother to take, a forklift nobody's fixed. He's trying to rebuild it on cheap hands and credit he doesn't have. The work floor's out front; his office is the glassed-in box in back, and you don't go in there until he wants you to.</p>
<<= setup.renderNpcPortraits("renner_depot")>>
<<= setup.renderSoloActivities("renner_depot")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave Renner's Depot->Location_the_waterfront]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "facility_ruins">>
<<if not $game_state.visited_locations.includes("facility_ruins")>>
<<set $game_state.visited_locations.push("facility_ruins")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("facility_ruins")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Facility</h2>
<p>What's left of the Vance asset-facility Cain burned to the ground — scorched frame, melted server racks, the smell of old smoke that never washed out. The first dread-place. Something in the wreckage is going to look like it belongs to her.</p>
<<= setup.renderNpcPortraits("facility_ruins")>>
<<= setup.renderSoloActivities("facility_ruins")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Facility->Location_the_waterfront]]<br>
</div>
</div>
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "the_site_open", "operator": "is_true"}]})>><<nobr>>
<<set $player.current_location = "the_site">>
<<if not $game_state.visited_locations.includes("the_site")>>
<<set $game_state.visited_locations.push("the_site")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("the_site")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Site</h2>
<p>Somewhere out past the container stacks — the place Cain just walked away from. She doesn't have it yet.</p>
<<= setup.renderNpcPortraits("the_site")>>
<<= setup.renderSoloActivities("the_site")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Site->Location_underworld_strip]]<br>
</div>
</div>
<</if>><<else>>
<h2>The Site</h2>
<p class="entry-blocked-narrative">Past the strip the city goes dark and deep, and that's where he is. The way down isn't open to her yet.</p>
[[Go back->Location_underworld_strip]]
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_hired", "operator": "is_true"}]})>><<nobr>>
<<set $player.current_location = "renner_burned_yard">>
<<if not $game_state.visited_locations.includes("renner_burned_yard")>>
<<set $game_state.visited_locations.push("renner_burned_yard")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("renner_burned_yard")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Burned Yard</h2>
<p>Renner's old yard, three streets past the depot — what Cain left of it. Torched to the slab, fence down, but his men still walk it with lights, turning back anyone who comes to pick the bones. Whatever he's keeping out here, he keeps it watched.</p>
<<= setup.renderNpcPortraits("renner_burned_yard")>>
<<= setup.renderSoloActivities("renner_burned_yard")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Burned Yard->Location_the_waterfront]]<br>
</div>
</div>
<</if>><<else>>
<h2>The Burned Yard</h2>
<p class="entry-blocked-narrative">You've no reason to be out at his burned yard — not yet. Get inside his world first.</p>
[[Go back->Location_the_waterfront]]
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "salvage_entered", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "salvage_done", "operator": "is_false"}]})>><<nobr>>
<<set $player.current_location = "kess_berth">>
<<if not $game_state.visited_locations.includes("kess_berth")>>
<<set $game_state.visited_locations.push("kess_berth")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("kess_berth")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Berth</h2>
<p>A flooded breaking-dock cut in under the docks, off the strip — a stripped hull up on the blocks in black water, cutting-gear and parts bins and a work-cradle rigged for something bigger than a person. Whoever works down here takes machines apart for a living — and, for the right coin, puts them back.</p>
<<= setup.renderNpcPortraits("kess_berth")>>
<<= setup.renderSoloActivities("kess_berth")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Berth->Location_underworld_strip]]<br>
</div>
</div>
<</if>><<else>>
<h2>The Berth</h2>
<p class="entry-blocked-narrative">A breaking-dock off the strip, chained and dark. Whoever strips ships down here doesn't take walk-ins.</p>
[[Go back->Location_underworld_strip]]
<</if>><<nobr>>
<<set $player.current_location = "underworld_gate">>
<<if not $game_state.visited_locations.includes("underworld_gate")>>
<<set $game_state.visited_locations.push("underworld_gate")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("underworld_gate")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Underworld Gate</h2>
<p>A freight door under the docks that isn't on any manifest, and a man in front of it who is. Past him the city goes down instead of up — the part of the Reach the Spire pretends doesn't exist. He doesn't move for free.</p>
<<= setup.renderNpcPortraits("underworld_gate")>>
<<= setup.renderSoloActivities("underworld_gate")>>
<div class="location-navigation">
</div>
<</if>><<nobr>>
<<set $player.current_location = "underworld_strip">>
<<if not $game_state.visited_locations.includes("underworld_strip")>>
<<set $game_state.visited_locations.push("underworld_strip")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("underworld_strip")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Underground</h2>
<p>Down past the gate the city turns inside out — wet brick, red light, stalls selling what the Spire outlaws and bodies selling what it pretends to. Nobody here knows what she is, and nobody here owns her. For the length of a visit she's just another face with coin to make. The way back up is the gate.</p>
<<= setup.renderNpcPortraits("underworld_strip")>>
<<= setup.renderSoloActivities("underworld_strip")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("underworld_bar")>><a class="location-card link-internal" data-passage="Location_underworld_bar"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_bar.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Undertow</span><<if setup.getLocationCostTag("underworld_bar")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("underworld_bar")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("underworld_bar")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("underworld_bar")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_bar.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Undertow</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("underworld_bar")>></span></div></div><</if>><<if setup.navDestUnlocked("underworld_brothel")>><a class="location-card link-internal" data-passage="Location_underworld_brothel"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_brothel.jpg')"></div><div class="location-card-content"><span class="location-card-name">The House</span><<if setup.getLocationCostTag("underworld_brothel")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("underworld_brothel")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("underworld_brothel")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("underworld_brothel")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_brothel.jpg')"></div><div class="location-card-content"><span class="location-card-name">The House</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("underworld_brothel")>></span></div></div><</if>><<if setup.navDestUnlocked("underworld_pit")>><a class="location-card link-internal" data-passage="Location_underworld_pit"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_pit.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Pit</span><<if setup.getLocationCostTag("underworld_pit")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("underworld_pit")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("underworld_pit")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("underworld_pit")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_pit.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Pit</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("underworld_pit")>></span></div></div><</if>><<if setup.navDestUnlocked("underworld_market")>><a class="location-card link-internal" data-passage="Location_underworld_market"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_market.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Black Market</span><<if setup.getLocationCostTag("underworld_market")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("underworld_market")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("underworld_market")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("underworld_market")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_market.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Black Market</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("underworld_market")>></span></div></div><</if>><<if setup.navDestUnlocked("kess_berth")>><a class="location-card link-internal" data-passage="Location_kess_berth"><div class="location-card-image" style="background-image: url('./videos/locations/kess_berth.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Berth</span><<if setup.getLocationCostTag("kess_berth")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("kess_berth")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("kess_berth")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("kess_berth")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/kess_berth.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Berth</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("kess_berth")>></span></div></div><</if>><<if setup.navDestUnlocked("crew_den")>><a class="location-card link-internal" data-passage="Location_crew_den"><div class="location-card-image" style="background-image: url('./videos/locations/crew_den.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Crew's Rooms</span><<if setup.getLocationCostTag("crew_den")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("crew_den")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("crew_den")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("crew_den")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/crew_den.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Crew's Rooms</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("crew_den")>></span></div></div><</if>><<if setup.navDestUnlocked("the_site")>><a class="location-card link-internal" data-passage="Location_the_site"><div class="location-card-image" style="background-image: url('./videos/locations/the_site.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Site</span><<if setup.getLocationCostTag("the_site")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("the_site")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("the_site")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("the_site")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/the_site.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Site</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("the_site")>></span></div></div><</if>></div><</nobr>>
</div>
<</if>><<nobr>>
<<set $player.current_location = "underworld_brothel">>
<<if not $game_state.visited_locations.includes("underworld_brothel")>>
<<set $game_state.visited_locations.push("underworld_brothel")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("underworld_brothel")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The House</h2>
<p>A working house off the strip — rooms by the hour, a madam who counts heads and coin. They'll take her on a shift; down here the thing the company uses her for is just a trade, and the coin is hers.</p>
<<= setup.renderNpcPortraits("underworld_brothel")>>
<<= setup.renderSoloActivities("underworld_brothel")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The House->Location_underworld_strip]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "underworld_pit">>
<<if not $game_state.visited_locations.includes("underworld_pit")>>
<<set $game_state.visited_locations.push("underworld_pit")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("underworld_pit")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Pit</h2>
<p>A sunken ring under a warehouse, a crowd around it laying bets. They'll match her against whoever's standing. Win and the purse is coin; lose and you crawl out. Down here her fists earn the same as her body.</p>
<<= setup.renderNpcPortraits("underworld_pit")>>
<<= setup.renderSoloActivities("underworld_pit")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Pit->Location_underworld_strip]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "underworld_market">>
<<if not $game_state.visited_locations.includes("underworld_market")>>
<<set $game_state.visited_locations.push("underworld_market")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("underworld_market")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Black Market</h2>
<p>Stalls and back-rooms where coin buys what's off every shelf above — weapons, gear, and people, penned at the far end like the rest of the stock. She knows the look of the merchandise better than she'd like.</p>
<<= setup.renderNpcPortraits("underworld_market")>>
<<= setup.renderSoloActivities("underworld_market")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Black Market->Location_underworld_strip]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "underworld_bar">>
<<if not $game_state.visited_locations.includes("underworld_bar")>>
<<set $game_state.visited_locations.push("underworld_bar")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("underworld_bar")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Undertow</h2>
<p>A bar off the strip, quieter than the rest of the underground — low light, older money, men who talk in the corners and go still when you pass. It runs cleaner than a place this deep has any right to, and it plainly belongs to someone; nobody says who. Sol works the taps and hears all of it.</p>
<<= setup.renderNpcPortraits("underworld_bar")>>
<<= setup.renderSoloActivities("underworld_bar")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Undertow->Location_underworld_strip]]<br>
</div>
</div>
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_true"}]})>><<nobr>>
<<set $player.current_location = "crew_den">>
<<if not $game_state.visited_locations.includes("crew_den")>>
<<set $game_state.visited_locations.push("crew_den")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("crew_den")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Crew's Rooms</h2>
<p>A dead man's rooms down past the market, off a stair nobody's cleared — where Doss and Rourke and the rest holed up before Renner put them in the ground. The door still answers to a key under the third stair, and nobody's been back. Nobody but her.</p>
<<= setup.renderNpcPortraits("crew_den")>>
<<= setup.renderSoloActivities("crew_den")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Crew's Rooms->Location_underworld_strip]]<br>
</div>
</div>
<</if>><<else>>
<h2>The Crew's Rooms</h2>
<p class="entry-blocked-narrative">There's a crew that holed up down past the market — but she doesn't have the address yet. Marsh has it.</p>
[[Go back->Location_underworld_strip]]
<</if>><<nobr>>
<<set $player.current_location = "captive_room">>
<<if not $game_state.visited_locations.includes("captive_room")>>
<<set $game_state.visited_locations.push("captive_room")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("captive_room")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Room</h2>
<p>Concrete, painted once, a long time ago. A bed bolted to the floor and a chair that isn't. A table with nothing on it. A drain in the middle of the floor, because whoever built this expected to hose it out. The light stays on. There is a door.</p>
<<= setup.renderNpcPortraits("captive_room")>>
<<= setup.renderSoloActivities("captive_room")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("captive_door")>><a class="location-card link-internal" data-passage="Location_captive_door"><div class="location-card-image" style="background-image: url('./videos/locations/captive_door.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Door</span><<if setup.getLocationCostTag("captive_door")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("captive_door")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("captive_door")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("captive_door")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/captive_door.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Door</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("captive_door")>></span></div></div><</if>></div><</nobr>>
</div>
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "trait", "subject": "player", "trait_key": "cell_door", "operator": "gte", "value": 1}]})>><<nobr>>
<<set $player.current_location = "captive_door">>
<<if not $game_state.visited_locations.includes("captive_door")>>
<<set $game_state.visited_locations.push("captive_door")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("captive_door")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Door</h2>
<p>Steel, and newer than the room. It opens when they want it to.</p>
<<= setup.renderNpcPortraits("captive_door")>>
<<= setup.renderSoloActivities("captive_door")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Door->Location_captive_room]]<br>
</div>
</div>
<</if>><<else>>
<h2>The Door</h2>
<p class="entry-blocked-narrative">The door does not open for her.</p>
[[Go back->Location_captive_room]]
<</if>>
<!-- STORY CANVAS PASSAGES --><p>Dev jump: seeding the post-Salvage state (Leash: Uncut, Calloway dispatched) and dropping at the Spire. Step into Vance Securities to open the Archive.</p>
<<set $game_state.current_canvas = "dev_jump_archive_start">>
<<set $game_state.current_node = "dev_jump_archive_start.seed">>
<<script>>setup.markCanvasTriggered("dev_jump_archive_start");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("dev_jump_archive_start.seed") === -1) { vn.push("dev_jump_archive_start.seed"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 1.0, false, null);setup.applyAndNotifyTrait("player", null, "core_strain", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "core_sealed", "set", 1.0, false, null);setup.applyAndNotifyTrait("player", null, "repair_session", "set", 12.0, false, null);setup.applyAndNotifyTrait("player", null, "calloway_drains_done", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "coin", "set", 100.0, false, null);setup.applyAndNotifyTrait("player", null, "energy", "set", 100.0, false, null);setup.applyAndNotifyTrait("player", null, "names_known", "set", 1.0, false, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "opening_done", "set");setup.applyAndNotifyFlag("player", null, "renner_drained", "set");setup.applyAndNotifyFlag("player", null, "hunt_lead", "set");setup.applyAndNotifyFlag("player", null, "crew_known", "set");setup.applyAndNotifyFlag("player", null, "bastien_found", "set");setup.applyAndNotifyFlag("player", null, "bastien_revealed", "set");setup.applyAndNotifyFlag("player", null, "captivity_entered", "set");setup.applyAndNotifyFlag("player", null, "captivity_broken", "set");setup.applyAndNotifyFlag("player", null, "captivity_done", "set");setup.applyAndNotifyFlag("player", null, "salvage_entered", "set");setup.applyAndNotifyFlag("player", null, "kess_met", "set");setup.applyAndNotifyFlag("player", null, "salvage_done", "set");setup.applyAndNotifyFlag("player", null, "mission_3_active", "set");setup.applyAndNotifyFlag("player", null, "salvage_relaunched", "set");setup.applyAndNotifyFlag("player", null, "mercer_hub_open", "set");<</script>>
<<script>>setup.pendingEffects = setup.pendingEffects || [];setup.addToWardrobe("cover_analyst"); setup.equipItem("cover_analyst");setup.showEffectNotification();<</script>>
[[To the Spire.->Location_spire_plaza]]<p>Dev jump: seeding a post-captivity state and dropping on the waterfront. The Salvage cold-open fires next.</p>
<<set $game_state.current_canvas = "dev_jump_salvage_start">>
<<set $game_state.current_node = "dev_jump_salvage_start.seed">>
<<script>>setup.markCanvasTriggered("dev_jump_salvage_start");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("dev_jump_salvage_start.seed") === -1) { vn.push("dev_jump_salvage_start.seed"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 1.0, false, null);setup.applyAndNotifyTrait("player", null, "core_strain", "set", 96.0, false, null);setup.applyAndNotifyTrait("player", null, "energy", "set", 80.0, false, null);setup.applyAndNotifyTrait("player", null, "names_known", "set", 1.0, false, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "opening_done", "set");setup.applyAndNotifyFlag("player", null, "renner_drained", "set");setup.applyAndNotifyFlag("player", null, "hunt_lead", "set");setup.applyAndNotifyFlag("player", null, "crew_known", "set");setup.applyAndNotifyFlag("player", null, "bastien_found", "set");setup.applyAndNotifyFlag("player", null, "bastien_revealed", "set");setup.applyAndNotifyFlag("player", null, "captivity_entered", "set");setup.applyAndNotifyFlag("player", null, "captivity_broken", "set");setup.applyAndNotifyFlag("player", null, "captivity_done", "set");<</script>>
[[Onto the dock.->Location_the_waterfront]]<p>Dev jump: seeding a near-broken captivity state and dropping into the cell. Lie down once to tip the break.</p>
<<set $game_state.current_canvas = "dev_jump_captivity_break">>
<<set $game_state.current_node = "dev_jump_captivity_break.seed">>
<<script>>setup.markCanvasTriggered("dev_jump_captivity_break");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("dev_jump_captivity_break.seed") === -1) { vn.push("dev_jump_captivity_break.seed"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "core_strain", "set", 95.0, false, null);setup.applyAndNotifyTrait("player", null, "energy", "set", 30.0, false, null);setup.applyAndNotifyTrait("player", null, "names_known", "set", 1.0, false, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "opening_done", "set");setup.applyAndNotifyFlag("player", null, "renner_drained", "set");setup.applyAndNotifyFlag("player", null, "hunt_lead", "set");setup.applyAndNotifyFlag("player", null, "crew_known", "set");setup.applyAndNotifyFlag("player", null, "bastien_found", "set");setup.applyAndNotifyFlag("player", null, "bastien_revealed", "set");setup.applyAndNotifyFlag("player", null, "captivity_entered", "set");<</script>>
[[Into the cell.->Location_captive_room]]<p>Dev jump: seeding the hunt chain and dropping into the grab.</p>
<<set $game_state.current_canvas = "dev_jump_bastien_grab">>
<<set $game_state.current_node = "dev_jump_bastien_grab.seed">>
<<script>>setup.markCanvasTriggered("dev_jump_bastien_grab");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("dev_jump_bastien_grab.seed") === -1) { vn.push("dev_jump_bastien_grab.seed"); }<</script>>
<<nobr>>
<<link "Jump to the grab" "Canvas_hunt_the_grab_Node_base">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drain_charge", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drains_done", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "names_known", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "corruption", "set", 60.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "opening_done", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_fucked_once", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_anal_once", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_drained", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_leads_extracted", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "hunt_lead", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "brothel_hired", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "crew_known", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "bastien_found", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>Dev jump: seeding the drain state and dropping into the payload.</p>
<<set $game_state.current_canvas = "dev_jump_renner_drain">>
<<set $game_state.current_node = "dev_jump_renner_drain.seed">>
<<script>>setup.markCanvasTriggered("dev_jump_renner_drain");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("dev_jump_renner_drain.seed") === -1) { vn.push("dev_jump_renner_drain.seed"); }<</script>>
<<nobr>>
<<link "Jump to the drain" "Canvas_renner_control_canvas_Node_intro">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drain_charge", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drains_done", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "names_known", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "corruption", "set", 60.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "opening_done", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_fucked_once", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_anal_once", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_drained", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_leads_extracted", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/scenes/mercer_panic.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She comes up to the penthouse with the file to close the job — and finds Mercer on the wrong side of his own desk, a bag open on the glass, feeding paper into a shredder that can't keep up.</p><span id="cascade-b2-beat-0"><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> It's done. Deep vault, clean pull. Everything you sent me in there for.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Get it away from me. God — do you have any idea what you kicked over? A deep-vault flag with my asset's fingerprints on it, and it is climbing, and it does not stop until it reaches Vance himself.</div></div><span id="cascade-b2-beat-1"><<linkreplace "Watch him unravel.">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> You wanted this file for a year. Now you won't touch it.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> I wanted it when it was a clever little edge. It's a noose now. The Chairman doesn't discipline, he disappears people — I've watched him do it. I will not be standing here when he comes asking who ran an asset into his archive. Keep the file, burn it, I don't care. I need to be gone, and every road out of this tower has his people on it by now.</div></div><span id="cascade-b2-beat-2"><<linkreplace "Offer him the one road he hasn't got.">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Not every road. There's one place his people can't follow, because they don't know it's there. Under the docks. Under the Reach. I can walk you into it tonight and nobody topside ever sees where you went.</div></div><p>That stops his hands. He looks at her the way a drowning man looks at a rope he doesn't trust — because he knows her, knows nothing she offers is free.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> ...And what does that cost me.</div></div><span id="cascade-b2-beat-3"><<linkreplace "Name it.">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> The controller. The one for the governor you had sealed into my core — the little key that decides who I can't turn my teeth on. You've carried it since the day you signed for me. Give it to me and I take you down alive. That's the fare.</div></div><p>For a second the fear thins and something like a smile moves under it — the reflex of a man who's spent his life sure he holds the only card that matters.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> That's it? Take it. It's keyed to me, girl — my hand, my print, it's a dead lump of metal to anyone else alive. You can wear it on a chain. It won't cut a thing.</div></div><span id="cascade-b2-beat-4"><<linkreplace "Take the key. Take him down.">><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> He's wrong, and he'll die never knowing how wrong. Keyed to him, sure — until a man who takes machines apart for a living gets it under a light. She closes her hand around it and it's the first thing in two years that's hers and about her at once. The file in her coat, the key in her fist. Both locked. Both hers.</div></div><p>She gets him into the car and gives the driver an address that isn't on any manifest. The tower falls away, the city goes lower and wetter, and the Spire seals itself shut behind them — sweeps on every floor now, her face on every camera at the curb. No ride back up. At the bottom, where the last light gives out, Mercer goes his own way into the dark to find whatever hole a running man finds, and doesn't look back to thank her. The deal's paid; they're quits. She's alone in the underworld now with a locked key in her fist and a locked file in her coat — the whole of herself sealed in two objects she can't open. That's where her road runs out, for now.</p>[[Down into the Reach.->Location_underworld_strip]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "cap_1a_close">>
<<set $game_state.current_node = "cap_1a_close.base">>
<<script>>setup.markCanvasTriggered("cap_1a_close");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_1a_close.base") === -1) { vn.push("cap_1a_close.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "archive_1a_done", "set");setup.applyAndNotifyFlag("player", null, "controller_held", "set");<</script>><p>She catches Vane on the back stairs, where the cameras don't reach and Calloway never goes, and doesn't waste the opening on hello.</p><span id="cascade-b1-beat-0"><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> A courier at the Undertow gave you up. Not your name — he never had it — your face. Handing him sealed pulls at the loading dock, month after month. Two years Calloway's chased his ghost, and it's been at the next desk the whole time.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/vane.jpg" alt="Vane" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Vane:</strong> You don't— that's nothing. That's a drunk in a bar, you've got a face and a feeling and no—</div></div><span id="cascade-b1-beat-1"><<linkreplace "Show him the drop.">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> I've got the courier, the dock, the dates, and a very tired auditor upstairs who'd love a name that isn't Calloway's. I don't want you, Vane. I want one thing out of that vault, and you're cleared for it and I'm not.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/vane.jpg" alt="Vane" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Vane:</strong> ...Christ. The deep archive? Do you know what they do to— no. I move paper. I don't touch the vault, that's a different—</div></div><span id="cascade-b1-beat-2"><<linkreplace "Make it the only door left.">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> You pull my file, or the auditor gets your face by morning and it climbs to the Chairman by noon. You know what he does with a leak. My way, you're a man who did a quiet favour. The other way, you're an example.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/vane.jpg" alt="Vane" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Vane:</strong> ...One file. One. And then I have never met you.</div></div><span id="cascade-b1-beat-3"><<linkreplace "Wait for the file.">><p>It takes him two days and a clearance that isn't his to use. He brings it to the same dead stairwell — a thin sealed folder — and his hands aren't steady.</p><span id="cascade-b1-beat-4"><<linkreplace "Take the file.">><p>She has it: the thing Mercer sent her for, and the thing she's wanted in her own hands since she learned it was in this building. A Vance humanoid-program file — about her kind, maybe about her. Light enough to be nothing. She doesn't open it. Not here.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/vane.jpg" alt="Vane" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Vane:</strong> Don't read it here. And listen — pulling that tripped something. There's a flag on the deep-vault log I can't clear, and somebody's already asking who signed it out.</div></div><span id="cascade-b1-beat-5"><<linkreplace "Ask who's watching.">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Calloway's watcher. He never trusted me either.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/vane.jpg" alt="Vane" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Vane:</strong> Whoever it is, it climbs — and it climbs to the man who ends people for less than this. I'm gone tonight. The Reach, then further. You should be too. What you just did isn't the kind of thing you get to stay for.</div></div><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> So it's blown. Fine. She has the file and a night's head start, and a man who ends betrayers is now aimed at everyone who touched this — Vane, and the fool up the chain who sent her in. She'd better reach Mercer before the news does.</div></div>[[Get to Mercer.->Location_penthouse]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "cap_vane_blackmail">>
<<set $game_state.current_node = "cap_vane_blackmail.base">>
<<script>>setup.markCanvasTriggered("cap_vane_blackmail");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_vane_blackmail.base") === -1) { vn.push("cap_vane_blackmail.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "docs_retrieved", "set");<</script>><p>She comes back after hours, when the department runs on a skeleton and the floor above is dark — and there's Vane, badge on a lanyard, walking the vault door open like it's nothing. The reader goes green for him. A guard nods him through.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> Of course. Quiet, tidy, trusted Vane — senior custody, cleared for the one door that matters, in and out on his own routine while the division dies around him. Whatever she needs out of that vault doesn't come out without a man exactly like him.</div></div>
<<set $game_state.current_canvas = "cap_docs_vane_routine">>
<<set $game_state.current_node = "cap_docs_vane_routine.base">>
<<script>>setup.markCanvasTriggered("cap_docs_vane_routine");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_docs_vane_routine.base") === -1) { vn.push("cap_docs_vane_routine.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Watch him work.->Location_docs_department]]<img src="./videos/scenes/docs_department.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Calloway gave it up under the drain — the deep vault, down here — so she comes to see it for herself. The docs department is everything his file room isn't: glass, logged, watched. This is where the files that matter actually live, and where whatever Mercer planted her to fetch is sitting, behind a door with her name nowhere near the list.</p><span id="cascade-b2-beat-0"><p>She badges toward the vault like she belongs there. The reader blinks red. A guard steps half a pace to fill the gap — not even unfriendly about it.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/enns.jpg" alt="Enns" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Enns:</strong> That's two-man clearance, love. You're not on the list, and I can't put you on it — nobody at your level goes past me. Anything you need out of the deep archive, you file a pull and someone cleared brings it up.</div></div><span id="cascade-b2-beat-1"><<linkreplace "Ask who's cleared.">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> And who's cleared?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/enns.jpg" alt="Enns" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Enns:</strong> A handful. Senior custody only. They rotate the second man weekly so nobody can walk it out alone. It's the one part of this dying place they still take seriously.</div></div><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> So there's the wall. Her file is ten feet away behind a door her badge will never open, and the only way through is somebody already on the inside. She'll need one of the cleared few — which means she needs to know exactly who they are, and find the right thing to hold over one.</div></div>[[Back out.->Location_docs_department]]
<</linkreplace>></span></span>
<<set $game_state.current_canvas = "cap_docs_first_look">>
<<set $game_state.current_node = "cap_docs_first_look.base">>
<<script>>setup.markCanvasTriggered("cap_docs_first_look");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_docs_first_look.base") === -1) { vn.push("cap_docs_first_look.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "access_problem_seen", "set");<</script>><p>The whole compartmented, careful little mind of him comes loose down her arm while he's still shaking — routes and drops and hand-offs, months of them, every pickup he's ever made.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> There. The inside contact. Not a name — Colm was never trusted with a name — a FACE, handing him a sealed packet at a loading dock, month after month. Narrow, nervous, careful. The face she sits three feet from at Calloway's elbow every single day. Vane. It's Vane.</div></div><p>She leaves him face-down and snoring on the empties, his route still spilling out of him into nobody, and walks back up into the night with the one thing she came for. She has the rogue now. She just has to make him hand her the vault.</p>
<<set $game_state.current_canvas = "colm_drain_canvas">>
<<set $game_state.current_node = "colm_drain_canvas.d0">>
<<script>>setup.markCanvasTriggered("colm_drain_canvas");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("colm_drain_canvas.d0") === -1) { vn.push("colm_drain_canvas.d0"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Go collect on Vane.->Location_underworld_bar]]<p>The drain fires the way it does now — no discovery left in it, just his Charge coming loose down her arm and the night going soft behind his eyes. He'll wake on the empties grateful and none the wiser, sure he had the best night of his week. In a way he did.</p>
<<set $game_state.current_canvas = "colm_drain_canvas">>
<<set $game_state.current_node = "colm_drain_canvas.done">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("colm_drain_canvas.done") === -1) { vn.push("colm_drain_canvas.done"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Leave him to it.->Location_underworld_bar]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 0}]})>>
<p>He finishes on her face with one hand shaking in her hair, apologising while he does it. Nothing comes loose but him. The drain stays shut — it only opens the one way.</p><video src="./videos/sex/colm_finish_facial_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 1}]})>>
<p>He finishes inside her, holding her up against the shelving, breathing like a man who's just been given something. He has been. She hasn't taken anything — not this way.</p><video src="./videos/sex/colm_finish_inside_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}]})>>
<p>He finishes in her ass, and the drain opens under it the way it's built to — the shudder cracking him wide, her hand still locked on his forearm, everything he is starting to come loose down her arm.</p><video src="./videos/sex/colm_finish_anal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<</if>>
<<set $game_state.current_canvas = "loop_colm_finisher">>
<<set $game_state.current_node = "loop_colm_finisher.climax">>
<<script>>setup.markCanvasTriggered("loop_colm_finisher");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_colm_finisher.climax") === -1) { vn.push("loop_colm_finisher.climax"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "lt", "value": 2}]})>>
<span @class="setup.isChoiceVisited('loop_colm_finisher:cc0') ? '' : 'unlocked-choice'">
<<link "Leave him to the empties." "Location_underworld_bar">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_colm_finisher:cc0");<</script>><<script>>advanceTime(60);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "colm_drains_done", "operator": "lt", "value": 1}]})>>
<span @class="setup.isChoiceVisited('loop_colm_finisher:cc1') ? '' : 'unlocked-choice'">
<<link "Take what he carries." "Canvas_colm_drain_canvas_Node_d0">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "colm_drains_done", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_colm_finisher:cc1");<</script>><<script>>advanceTime(60);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "colm_drains_done", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('loop_colm_finisher:cc2') ? '' : 'unlocked-choice'">
<<link "Drink him down again." "Canvas_colm_drain_canvas_Node_done">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_colm_finisher:cc2");<</script>><<script>>advanceTime(60);<</script>><</link>><br>
</span>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "lt", "value": 2}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "colm_drains_done", "operator": "lt", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "colm_drains_done", "operator": "gte", "value": 1}]})))>>
<<script>>
try {
window._engineNoExitsDiag_loop_colm_finisher = [{"text": "Leave him to the empties.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "lt", "value": 2}]}}, {"text": "Take what he carries.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "colm_drains_done", "operator": "lt", "value": 1}]}}, {"text": "Drink him down again.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "colm_drains_done", "operator": "gte", "value": 1}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "loop_colm_finisher",
choices: window._engineNoExitsDiag_loop_colm_finisher,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_loop_colm_finisher>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas loop_colm_finisher</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Navigation]]
<</if>>
<</nobr>><img src="./videos/sex/colm_backroom.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The back room is empties and stacked cases and one bad bulb. He has her against the shelving before the door's shut — mouth on her neck, hands going everywhere at once, a careful man with nothing careful left in him.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> Three weeks I've been telling myself you were being kind. Tell me to stop and I'll stop. Don't, and I won't.</div></div>
<<set $game_state.current_canvas = "loop_colm_backroom">>
<<set $game_state.current_node = "loop_colm_backroom.intro">>
<<script>>setup.markCanvasTriggered("loop_colm_backroom");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_colm_backroom.intro") === -1) { vn.push("loop_colm_backroom.intro"); }<</script>>
<<nobr>>
<<link "Get on your knees." "Canvas_loop_colm_backroom_Node_base_oral_colm">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/colm_loop_oral_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>On her knees on the concrete with his cock in her mouth, both his hands braced on the case behind him. He watches the door out of thirty years of habit, and loses the habit fast.</p>
<<set $game_state.current_canvas = "loop_colm_backroom">>
<<set $game_state.current_node = "loop_colm_backroom.base_oral_colm">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_colm_backroom.base_oral_colm") === -1) { vn.push("loop_colm_backroom.base_oral_colm"); }<</script>>
<<nobr>>
<<link "Keep him in your mouth." "Canvas_loop_colm_backroom_Node_base_oral_colm">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Stand up and put him in you." "Canvas_loop_colm_backroom_Node_base_vaginal_colm">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('loop_colm_backroom:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_loop_colm_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_colm_backroom:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/colm_loop_vaginal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She takes him standing, one leg hooked up on a stacked case, his cock working into her cunt while the bar noise comes through the wall. He says her false name into her hair like it's a real one.</p>
<<set $game_state.current_canvas = "loop_colm_backroom">>
<<set $game_state.current_node = "loop_colm_backroom.base_vaginal_colm">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_colm_backroom.base_vaginal_colm") === -1) { vn.push("loop_colm_backroom.base_vaginal_colm"); }<</script>>
<<nobr>>
<<link "Let him have it." "Canvas_loop_colm_backroom_Node_base_vaginal_colm">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back down on your knees." "Canvas_loop_colm_backroom_Node_base_oral_colm">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Turn around — give him your ass." "Canvas_loop_colm_backroom_Node_base_anal_colm">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('loop_colm_backroom:cc1') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_loop_colm_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_colm_backroom:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/colm_loop_anal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Bent over the stacked cases with his cock in her ass, his hands white-knuckled on her hips. He's forgotten the door, the room, the job — everything but the fact that she let him.</p><p>She reaches back and gets a hand around his forearm, skin on skin, and holds it there. That's the seat. The drain sits in the contact and waits for the shudder to open it.</p>
<<set $game_state.current_canvas = "loop_colm_backroom">>
<<set $game_state.current_node = "loop_colm_backroom.base_anal_colm">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_colm_backroom.base_anal_colm") === -1) { vn.push("loop_colm_backroom.base_anal_colm"); }<</script>>
<<nobr>>
<<link "Take it in the ass." "Canvas_loop_colm_backroom_Node_base_anal_colm">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back on him." "Canvas_loop_colm_backroom_Node_base_vaginal_colm">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('loop_colm_backroom:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass — and take what he carries." "Canvas_loop_colm_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_colm_backroom:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><img src="./videos/scenes/colm_undertow.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "colm_met", "operator": "is_false"}]})>>
<p>Three stools down: the man Sol pointed her to. Corner seat, back to the wall, a bag with a boot hooked through the strap so it never leaves his ankle. He's nursing one drink like it has to last till dawn, and his eyes go to the door on a clock she can't hear — every ninety seconds, reflex, the tic of a man who's counted the exits in every room he's ever sat in. She takes the stool beside him. He clocks her, weighs her, and gives her exactly nothing — polite, careful, a man having a quiet one and meaning to keep it that way.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> If you're selling, I'm not buying. If you're asking, I'm that even less. Nothing personal — just how I've stayed a man who gets to keep having quiet ones.</div></div><p>But he doesn't move stools, and he doesn't tell her to leave. He just goes back to watching the door, letting her sit inside the arm's length he gives nobody — which is its own kind of answer. Careful men notice careful people. She hasn't asked him for a single thing; that's the only reason she's still got the seat.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> Careful to a fault, exactly like Sol said — and lonely with it, under all that guard. You don't crack a man like this with a question. You crack him with time. The first hour starts with a drink he didn't have to pay for.</div></div>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "colm_met", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "lt", "value": 12}]})>>
<p>He's still all business, but he lets her take the stool now without the once-over. A careful man testing the shape of her — what she wants, why a woman like her keeps spending her evenings on a courier three seats from the door.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> Nobody buys a man this many rounds for the conversation. So what is it — you want something moved, or you want me?</div></div>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "colm_met", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "gte", "value": 12}, {"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "lt", "value": 24}]})>>
<p>He saves her the stool now. The guard's not gone — a careful man's never fully goes — but he's loose, warm, leaning in to hear her over the room. He watches the door out of habit and her out of want.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> You're the best part of my week, you know that. Whatever it is you actually came for, I'm starting not to care.</div></div>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "colm_met", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "gte", "value": 24}]})>>
<p>The careful's all the way off him now. He's up off the stool before she's fully in the door, hand at the small of her back, steering her to the dark end of the bar like the room's already theirs. He'll follow her anywhere she points him — and she knows exactly where that is.</p>
<</if>>
<<set $game_state.current_canvas = "hub_colm_undertow">>
<<set $game_state.current_node = "hub_colm_undertow.base">>
<<script>>setup.markCanvasTriggered("hub_colm_undertow");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_colm_undertow.base") === -1) { vn.push("hub_colm_undertow.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "colm_met", "operator": "is_false"}]})>>
<span @class="setup.isChoiceVisited('hub_colm_undertow:cc0') ? '' : 'unlocked-choice'">
<<link "Can I buy you a drink?" "Canvas_hub_colm_undertow_Node_meet_drink">><<script>>setup.markChoiceVisited("hub_colm_undertow:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "colm_met", "operator": "is_false"}]})>>
<span @class="setup.isChoiceVisited('hub_colm_undertow:cc1') ? '' : 'unlocked-choice'">
<<link "Not tonight." "Location_underworld_bar">><<script>>setup.markChoiceVisited("hub_colm_undertow:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "colm_met", "operator": "is_true"}]})>>
<span @class="setup.isChoiceVisited('hub_colm_undertow:cc2') ? '' : 'unlocked-choice'">
<<link "Pass the time with him." "Canvas_hub_colm_undertow_Node_talk">><<script>>setup.markChoiceVisited("hub_colm_undertow:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "colm_met", "operator": "is_true"}]})>>
<span @class="setup.isChoiceVisited('hub_colm_undertow:cc3') ? '' : 'unlocked-choice'">
<<link "Buy him another." "Canvas_hub_colm_undertow_Node_drink">><<script>>setup.markChoiceVisited("hub_colm_undertow:cc3");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "colm_met", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "gte", "value": 12}]})>>
<span @class="setup.isChoiceVisited('hub_colm_undertow:cc4') ? '' : 'unlocked-choice'">
<<link "Kiss him." "Canvas_hub_colm_undertow_Node_kiss">><<script>>setup.markChoiceVisited("hub_colm_undertow:cc4");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "colm_met", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "gte", "value": 24}]})>>
<span @class="setup.isChoiceVisited('hub_colm_undertow:cc5') ? '' : 'unlocked-choice'">
<<link "Take him in the back." "Canvas_loop_colm_backroom_Node_intro">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hub_colm_undertow:cc5");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<link "Leave him to his drink." "Location_underworld_bar">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She flags Sol for two and slides one in front of him before he can wave it off. Colm looks at the glass, looks at her, and does the arithmetic of a man who trusts nothing free — what it costs him to drink it against what it costs him to refuse. Refusing is the louder tell. He drinks.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> ...Appreciated. Doesn't buy you anything but the drink, understand. But — appreciated.</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> No sell, no ask. Just the only man in here who looks like he'd still be alive this time next year — and I'm tired of drinking alone.</div></div><p>That earns her a look, a real one, held a beat longer than he means to give it — two careful people clocking the same wariness in each other. He says nothing else worth keeping. But he lets a whole minute pass without checking the door, and for a man like Colm that's most of a confession.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> One drink in and he's still all edges. Fine — she's got nowhere to be and he's here every night. Somewhere under the careful there's a courier who moves things between the worlds for a buyer who pays on time. That's the thread. She just has to be patient enough to pull it, a round at a time.</div></div>
<<set $game_state.current_canvas = "hub_colm_undertow">>
<<set $game_state.current_node = "hub_colm_undertow.meet_drink">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_colm_undertow.meet_drink") === -1) { vn.push("hub_colm_undertow.meet_drink"); }<</script>>
<<script>>advanceTime(60);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("npc", "npc_colm", "relation", "add", 3.0, false, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "colm_met", "set");<</script>>
[[Let him nurse it. Come back.->Location_underworld_bar]]<<set _bp to random(0, 2)>>
<<if _bp is 0>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> You want to know how you last down here? You never carry what you can't drop and you never learn a name you don't need. I've forgotten more people than this bar's seen. On purpose.</div></div>
<<elseif _bp is 1>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> Buyer up top pays on time, never asks how, never says thanks. Best kind of work there is — the kind nobody wants to be caught talking about, me included. So we won't.</div></div>
<<else>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> Sol vouches for exactly nobody, and he lets you sit here. That's the only reference in this room worth a damn, and it's the only reason I haven't moved stools.</div></div>
<</if>><p>He talks around the work, never through it — but he talks, which for Colm is a door coming open a finger's width at a time. Each hour she spends not asking him for anything is an hour he trusts her a little more.</p>
<<set $game_state.current_canvas = "hub_colm_undertow">>
<<set $game_state.current_node = "hub_colm_undertow.talk">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_colm_undertow.talk") === -1) { vn.push("hub_colm_undertow.talk"); }<</script>>
<<script>>advanceTime(30);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("npc", "npc_colm", "relation", "add", 2.0, false, null);setup.showEffectNotification();<</script>>
[[Let it ride.->Location_underworld_bar]]<p>Another round, and the night wears on the way nights do down here — slow, red-lit, the room thinning around them. He drinks careful, always careful. But he drinks with her now, which for a man who's made a life of drinking alone is not nothing.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "lt", "value": 12}]})>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> I don't get sloppy — never have, not the job for it. But I'll drink what you're pouring. Beats the wall, and you don't talk half as much as the others who try to sit here.</div></div>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "gte", "value": 12}, {"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "lt", "value": 24}]})>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> You're turning into a habit. Only good kind there is, in my line — the sort nobody's supposed to know I've got. Don't make me regret it.</div></div>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "gte", "value": 24}]})>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> Keep buying and I keep forgetting to watch that door. Reckon you know that. Reckon that's the point — and I've stopped minding either way.</div></div>
<</if>>
<<set $game_state.current_canvas = "hub_colm_undertow">>
<<set $game_state.current_node = "hub_colm_undertow.drink">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_colm_undertow.drink") === -1) { vn.push("hub_colm_undertow.drink"); }<</script>>
<<script>>advanceTime(60);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("npc", "npc_colm", "relation", "add", 3.0, false, null);setup.showEffectNotification();<</script>>
[[One more, then.->Location_underworld_bar]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "lt", "value": 16}]})>>
<p>She leans in and kisses him, slow, in the middle of the crowded bar. He goes stiff — a careful man's whole body saying not here, not smart, not safe — and pulls back an inch before he lets himself have even a second of it.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> —not here. People see. You don't know who drinks in this room.</div></div>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "gte", "value": 16}, {"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "lt", "value": 20}]})>>
<p>This time he doesn't pull back as far. His hand comes up to her jaw like it decided on its own, and the careful takes a beat longer to win. He's warming — she can feel exactly where the guard thins.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/colm.jpg" alt="Colm" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Colm:</strong> You're going to get me killed. ...Do it again.</div></div>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_colm", "trait_key": "relation", "operator": "gte", "value": 20}]})>>
<p>Now he stops flinching entirely. He kisses her back like the room isn't there, one hand fisted in her coat, the careful drinker gone under a want he isn't managing anymore.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> There. The guard's off. One more lean and he'll follow her anywhere she points him — including the back, where the drain likes to work.</div></div>
<</if>>
<<set $game_state.current_canvas = "hub_colm_undertow">>
<<set $game_state.current_node = "hub_colm_undertow.kiss">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_colm_undertow.kiss") === -1) { vn.push("hub_colm_undertow.kiss"); }<</script>>
<<script>>advanceTime(30);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("npc", "npc_colm", "relation", "add", 4.0, false, null);setup.showEffectNotification();<</script>>
[[Let him breathe.->Location_underworld_bar]]<img src="./videos/scenes/underworld_bar_bartender.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Sol works the taps at the dim end of the bar, wiping a glass that's already clean. He's outlasted every owner this place has had — and it's had a few, though the one it answers to now never shows his face. He hears everything said over his counter down here and repeats none of it, unless you're the kind he's decided he likes. He's decided he likes her. Mostly.</p>
<<set $game_state.current_canvas = "hub_sol_undertow">>
<<set $game_state.current_node = "hub_sol_undertow.base">>
<<script>>setup.markCanvasTriggered("hub_sol_undertow");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_sol_undertow.base") === -1) { vn.push("hub_sol_undertow.base"); }<</script>>
<<nobr>>
<<link "Pass a word with Sol." "Canvas_hub_sol_undertow_Node_talk">><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "pipe_known", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "colm_found", "operator": "is_false"}]})>>
<span @class="setup.isChoiceVisited('hub_sol_undertow:cc0') ? '' : 'unlocked-choice'">
<<link "Ask who moves things between the worlds." "Canvas_hub_sol_undertow_Node_carrier">><<script>>setup.markChoiceVisited("hub_sol_undertow:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "names_known", "operator": "gte", "value": 1}, {"type": "flag", "subject": "player", "flag_key": "hunt_lead", "operator": "is_false"}]})>>
<span @class="setup.isChoiceVisited('hub_sol_undertow:cc1') ? '' : 'unlocked-choice'">
<<link "Ask him about Doss and Rourke." "Canvas_hub_sol_undertow_Node_crew">><<script>>setup.markChoiceVisited("hub_sol_undertow:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<link "Nothing tonight." "Location_underworld_bar">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><<set _bp to random(0, 2)>>
<<if _bp is 0>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/sol.jpg" alt="Sol" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Sol:</strong> Rule of the Undertow: down here nobody's who they were up there. Man walks in, whatever he did in the daylight stays at the door. Keeps the peace. Keeps my bar standing.</div></div>
<<elseif _bp is 1>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/sol.jpg" alt="Sol" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Sol:</strong> Bar's owned by somebody now who never comes down to look at it. Suits me. An owner you never see is an owner who never asks questions you can't answer.</div></div>
<<else>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/sol.jpg" alt="Sol" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Sol:</strong> You want to last down here, you learn what I learned: hear it all, hold it all, hand out none of it for free. That's not being cold. That's the rent.</div></div>
<</if>><p>He talks the way a man talks who's decided you're all right — not warm, exactly, but no longer watching his own mouth. Down here that's most of the way to a friend.</p>
<<set $game_state.current_canvas = "hub_sol_undertow">>
<<set $game_state.current_node = "hub_sol_undertow.talk">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_sol_undertow.talk") === -1) { vn.push("hub_sol_undertow.talk"); }<</script>>
<<script>>advanceTime(15);<</script>>
[[Let him work.->Location_underworld_bar]]<div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> I need a carrier. Somebody who moves freight both ways — outer world to under, under to outer — and never leaves it on paper. Company work, the kind that never happened.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/sol.jpg" alt="Sol" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Sol:</strong> Short list down here, and a shorter one that's any good. The one you want's called Colm. Runs sealed packets for a buyer who pays too well to name, and hasn't let a thing show up in a ledger in all the years I've watched him drink. Careful to a fault.</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Where do I find him.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/sol.jpg" alt="Sol" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Sol:</strong> Right here, most nights — corner stool, back to the wall, drinking slow so he stays sharp. In around seven, nurses it till close. Buy the man a drink. He warms slower than he'll ever admit, but he warms.</div></div><p>A name, a stool, a habit — the whole of it. She looks down the bar and finds him without trying: a wary regular three stools down, a bag with a boot hooked through the strap, eyes on the door out of pure reflex. Her thread up out of Calloway's file room and straight to whoever's buying.</p>
<<set $game_state.current_canvas = "hub_sol_undertow">>
<<set $game_state.current_node = "hub_sol_undertow.carrier">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_sol_undertow.carrier") === -1) { vn.push("hub_sol_undertow.carrier"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "colm_found", "set");<</script>>
[[Colm, then.->Location_underworld_bar]]<div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Two men. Doss and Rourke. They came up out of the underworld before they ever ran for Renner. You'd have known them.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/sol.jpg" alt="Sol" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Sol:</strong> Doss I knew. Ran cargo for the company back when it still pretended it didn't. Renner made an example of him — him and Rourke both, and it took. But Doss ran with a third. A fixer, name of Marsh. Careful little man, and still breathing.</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Where.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/sol.jpg" alt="Sol" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Sol:</strong> He keeps one habit. The House, every Sunday — same girl, like clockwork. That's the only place Marsh surfaces. A man with one habit's got one weakness.</div></div><p>He sets the clean glass down. A name, a day, a door — the whole of it. Then, not looking up: "Whatever you carry that does your quiet work — carry it Sunday. Marsh doesn't talk twice."</p>
<<set $game_state.current_canvas = "hub_sol_undertow">>
<<set $game_state.current_node = "hub_sol_undertow.crew">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_sol_undertow.crew") === -1) { vn.push("hub_sol_undertow.crew"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "hunt_lead", "set");<</script>>
[[Sundays, then. Carry the drain.->Location_underworld_bar]]<p>The drain opens all the way and she takes him properly — not a skim, the whole live current of him coming loose through the contact. His Charge, his week, the last hour, going soft and white while she holds the pull steady and drinks it down.</p><p>And while he's this open, this undefended, she asks him the one thing she came for: where a company that never loses a page keeps what it can't afford to lose. He gives it up without knowing he's giving it — the deep vault, down in the docs department, the sealed archive nobody at her level ever badges into. Whatever Mercer planted her to fetch is sitting behind that door.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> So now she has the where — and a leash on the man who gave it to her. She can open him whenever the drain's hungry and he'll thank her for it; he'll never remember telling her a thing. Easier to keep him than to open that vault — and that part she'll have to solve herself.</div></div><p>He blinks, the minutes wiped, disoriented in his own chair. 'Sorry — lost the thread. Where were we.' She tells him it's nothing. Drink some water.</p>
<<set $game_state.current_canvas = "calloway_drain_canvas">>
<<set $game_state.current_node = "calloway_drain_canvas.d0">>
<<script>>setup.markCanvasTriggered("calloway_drain_canvas");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("calloway_drain_canvas.d0") === -1) { vn.push("calloway_drain_canvas.d0"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Straighten up.->Location_vance_securities]]<p>The drain fires the way it always does now — no drama, no discovery, just his Charge coming loose into her and the minutes going with it. He surfaces grateful and none the wiser, his hand finding hers like it's the only true thing in the room. She lets him think it is.</p>
<<set $game_state.current_canvas = "calloway_drain_canvas">>
<<set $game_state.current_node = "calloway_drain_canvas.done">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("calloway_drain_canvas.done") === -1) { vn.push("calloway_drain_canvas.done"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Leave him to it.->Location_vance_securities]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 0}]})>>
<p>He finishes on her face with a sound that's half relief, one hand cradling her head like she's the first good thing in two years. He'd give her anything now. He doesn't know what she's about to take.</p><video src="./videos/sex/calloway_finish_facial_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 1}]})>>
<p>He finishes inside her, holding her down on him, shaking, and after he just breathes into her neck — wrecked, grateful, believed. She lets him have it. Letting him have it is how she keeps him.</p><video src="./videos/sex/calloway_finish_inside_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}]})>>
<p>He finishes in her ass, and the drain opens under it the way it's built to — his whole self coming loose through the contact, the last few minutes already going soft and white behind his eyes.</p><video src="./videos/sex/calloway_finish_anal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<</if>>
<<set $game_state.current_canvas = "loop_calloway_finisher">>
<<set $game_state.current_node = "loop_calloway_finisher.climax">>
<<script>>setup.markCanvasTriggered("loop_calloway_finisher");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_calloway_finisher.climax") === -1) { vn.push("loop_calloway_finisher.climax"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "lt", "value": 2}]})>>
<span @class="setup.isChoiceVisited('loop_calloway_finisher:cc0') ? '' : 'unlocked-choice'">
<<link "Straighten up." "Location_vance_securities">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_calloway_finisher:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "calloway_drains_done", "operator": "lt", "value": 1}]})>>
<span @class="setup.isChoiceVisited('loop_calloway_finisher:cc1') ? '' : 'unlocked-choice'">
<<link "Open the drain on him." "Canvas_calloway_drain_canvas_Node_d0">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "calloway_drains_done", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_calloway_finisher:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "calloway_drains_done", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('loop_calloway_finisher:cc2') ? '' : 'unlocked-choice'">
<<link "Take him again." "Canvas_calloway_drain_canvas_Node_done">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_calloway_finisher:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "lt", "value": 2}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "calloway_drains_done", "operator": "lt", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "calloway_drains_done", "operator": "gte", "value": 1}]})))>>
<<script>>
try {
window._engineNoExitsDiag_loop_calloway_finisher = [{"text": "Straighten up.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "lt", "value": 2}]}}, {"text": "Open the drain on him.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "calloway_drains_done", "operator": "lt", "value": 1}]}}, {"text": "Take him again.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "calloway_drains_done", "operator": "gte", "value": 1}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "loop_calloway_finisher",
choices: window._engineNoExitsDiag_loop_calloway_finisher,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_loop_calloway_finisher>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas loop_calloway_finisher</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Navigation]]
<</if>>
<</nobr>><video src="./videos/sex/calloway_loop_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She shuts the file-room door for the last time as an analyst. He's already got his hands in her hair — the disbelieved man gone, just someone who's decided to trust the one person who ever let him.</p>
<<set $game_state.current_canvas = "loop_calloway_sex">>
<<set $game_state.current_node = "loop_calloway_sex.intro">>
<<script>>setup.markCanvasTriggered("loop_calloway_sex");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_calloway_sex.intro") === -1) { vn.push("loop_calloway_sex.intro"); }<</script>>
<<nobr>>
<<link "Get on your knees." "Canvas_loop_calloway_sex_Node_base_oral_c">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/calloway_loop_oral_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>On her knees with his cock in her mouth, and even now he half-talks — the ghost, the case, the shape of the thing he's chased for two years — because the work is the only place he's ever felt safe, and she's made herself the second.</p>
<<set $game_state.current_canvas = "loop_calloway_sex">>
<<set $game_state.current_node = "loop_calloway_sex.base_oral_c">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_calloway_sex.base_oral_c") === -1) { vn.push("loop_calloway_sex.base_oral_c"); }<</script>>
<<nobr>>
<<link "Keep him in your mouth." "Canvas_loop_calloway_sex_Node_base_oral_c">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Climb on." "Canvas_loop_calloway_sex_Node_base_vaginal_c">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('loop_calloway_sex:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_loop_calloway_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_calloway_sex:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/calloway_loop_vaginal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She rides him in the desk chair, slow, and he holds her face in both hands like he's checking she's real. He tells her the true name of the rogue he's chasing. She files it. She files everything.</p>
<<set $game_state.current_canvas = "loop_calloway_sex">>
<<set $game_state.current_node = "loop_calloway_sex.base_vaginal_c">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_calloway_sex.base_vaginal_c") === -1) { vn.push("loop_calloway_sex.base_vaginal_c"); }<</script>>
<<nobr>>
<<link "Take him." "Canvas_loop_calloway_sex_Node_base_vaginal_c">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back down on him." "Canvas_loop_calloway_sex_Node_base_oral_c">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Turn around — give him your ass." "Canvas_loop_calloway_sex_Node_base_anal_c">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('loop_calloway_sex:cc1') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_loop_calloway_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_calloway_sex:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/calloway_loop_anal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Bent over the desk he's guarded for two years, his cock in her ass, both his hands gripping like she's the last solid thing in a building that's been cutting him loose. This is the act that opens the drain — the one place her power still works freely, on a man who isn't the one who owns her.</p>
<<set $game_state.current_canvas = "loop_calloway_sex">>
<<set $game_state.current_node = "loop_calloway_sex.base_anal_c">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_calloway_sex.base_anal_c") === -1) { vn.push("loop_calloway_sex.base_anal_c"); }<</script>>
<<nobr>>
<<link "Take it in the ass." "Canvas_loop_calloway_sex_Node_base_anal_c">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back on him." "Canvas_loop_calloway_sex_Node_base_vaginal_c">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('loop_calloway_sex:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish — and take what he knows." "Canvas_loop_calloway_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_calloway_sex:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><img src="./videos/scenes/calloway_flinch.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>He stops her hand mid-reach and holds it — not gentle.</p><span id="cascade-b2-beat-0"><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> Why you. Why now. Nobody in this building has been decent to me in two years and then you walk in and you're this. What do you actually want. Who sent you.</div></div><p>It's the closest he's come to the truth, and he doesn't even know it.</p><span id="cascade-b2-beat-1"><<linkreplace "Re-anchor him.">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> You've been right for two years, and alone for two years. I'm not here to make you alone. I'm here because you're the only one who was ever right.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> ...God. I want that to be true.</div></div><p>He lets her hand go, and keeps it. The suspicion doesn't leave him. It just stops winning.</p>[[Let him have it.->Location_vance_securities]]
<</linkreplace>></span></span>
<<set $game_state.current_canvas = "cap_calloway_flinch">>
<<set $game_state.current_node = "cap_calloway_flinch.base">>
<<script>>setup.markCanvasTriggered("cap_calloway_flinch");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_calloway_flinch.base") === -1) { vn.push("cap_calloway_flinch.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "calloway_flinch_resolved", "set");<</script>><video src="./videos/scenes/rung_calloway_oral_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She takes him out from behind the desk and puts her mouth on him, and the sound he makes isn't lust — it's relief, like something clenched for two years finally letting go.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_calloway", "trait_key": "corruption", "operator": "lt", "value": 12}]})>>
<p>He tries to stay in his head — keeps talking, the case, the ghost, the names — even with his cock in her throat, because the work is the only place he's ever been safe. She lets him. She's good at making a man feel safe while she takes him apart.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_calloway", "trait_key": "corruption", "operator": "gte", "value": 12}]})>>
<p>Now he doesn't talk. He holds the back of her head, not pushing, just needing to know she's real, and she lets him fill her mouth and swallows it and calls it access.</p>
<</if>>
<<set $game_state.current_canvas = "rung_calloway_oral">>
<<set $game_state.current_node = "rung_calloway_oral.base">>
<<script>>setup.markCanvasTriggered("rung_calloway_oral");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rung_calloway_oral.base") === -1) { vn.push("rung_calloway_oral.base"); }<</script>>
<<script>>advanceTime(180);<</script>>
[[Wipe your mouth. Go.->Location_vance_securities]]<video src="./videos/scenes/rung_calloway_contact_t4.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>The file room, late. He's stopped pretending to work; she's stopped pretending to file. When she puts a hand flat on his chest he goes still.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_calloway", "trait_key": "corruption", "operator": "lt", "value": 6}]})>>
<p>Not the still of a man being seduced — the still of a man who's forgotten what it is to be touched by someone who isn't about to take something from him. She is, of course. He just doesn't know it yet.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> ...What are you doing.</div></div>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_calloway", "trait_key": "corruption", "operator": "gte", "value": 6}]})>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> You don't have to do this part. Whatever he's paying you to get out of me — not this part.</div></div><p>He says it like a man hoping to be argued with. She doesn't argue. She just doesn't stop, and he lets her not stop.</p>
<</if>>
<<set $game_state.current_canvas = "rung_calloway_contact">>
<<set $game_state.current_node = "rung_calloway_contact.base">>
<<script>>setup.markCanvasTriggered("rung_calloway_contact");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rung_calloway_contact.base") === -1) { vn.push("rung_calloway_contact.base"); }<</script>>
<<script>>advanceTime(180);<</script>>
[[Leave it there for tonight.->Location_vance_securities]]<p>With the sealed side of the hunt open to her now, she can finally see the far end — and the far end is a place Calloway has never once been.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> The pulls don't get sold up here. They go DOWN — into the Reach, through the kind of courier who never touches a ledger and never shows on a manifest. A company man could audit this room ten more years and never find him, because he isn't in any record there is. But she knows that world — and she knows who to ask. Down at the Undertow, Sol has poured for every carrier who ever worked the Reach; he'll know the man who runs this.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> I'll re-run the logistics side tomorrow. There's something in the courier contracts I keep missing — I can feel it.</div></div><p>She tells him that sounds right, and lets him keep chasing the ghost through paper. She already knows where she's going tonight: the Undertow, to put Sol's word to the man who carries it.</p>
<<set $game_state.current_canvas = "cap_case_pipe">>
<<set $game_state.current_node = "cap_case_pipe.base">>
<<script>>setup.markCanvasTriggered("cap_case_pipe");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_case_pipe.base") === -1) { vn.push("cap_case_pipe.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "pipe_known", "set");<</script>>
[[Head for the Reach.->Location_vance_securities]]<p>She stops chasing the leaks and starts reading the hunt itself — his two years of dead ends. And the dead ends have a pattern he's too close to see.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> The vendors, the couriers, the temp staff — I ran all of it. Ran my own people first, God help me, and they came back solid. Every thread just goes cold on me.</div></div><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> Every thread goes cold because the same hand keeps it cold. One of the few he trusts has been steering him off it for two years — vetting the lead that mattered, losing the record that pointed home. It's someone on his own team. She doesn't say it. The day she says it he's finished, and so is her way in.</div></div>
<<set $game_state.current_canvas = "cap_case_curated">>
<<set $game_state.current_node = "cap_case_curated.base">>
<<script>>setup.markCanvasTriggered("cap_case_curated");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_case_curated.base") === -1) { vn.push("cap_case_curated.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "case_curated_seen", "set");<</script>>
[[Say nothing. Keep going.->Location_vance_securities]]<p>A week of pulling boxes with him and the shape starts to show. She lays three of the leaked pulls side by side on his desk and taps them.</p><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> No forced drawer. No missing page. Every one of these was clean, and every one was exactly worth taking. Nobody broke in, Calloway. Somebody walked it out the front door.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> ...An inside job. Two years I've said it and the audit calls me paranoid because there's never a mark. You put it on my desk in ten days.</div></div>
<<set $game_state.current_canvas = "cap_case_insider">>
<<set $game_state.current_node = "cap_case_insider.base">>
<<script>>setup.markCanvasTriggered("cap_case_insider");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_case_insider.base") === -1) { vn.push("cap_case_insider.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "case_insider_seen", "set");<</script>>
[[Keep digging.->Location_vance_securities]]<img src="./videos/scenes/calloway_access.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She's in the room enough now that he talks while he works — and tonight he stops working, and just looks at her like he's deciding something.</p><span id="cascade-b2-beat-0"><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> Two years I've run this alone because nobody believed there was anything to run. You do. So I'm going to show you the part I don't show anyone — the pulls I never logged, the names I couldn't prove.</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Show me all of it.</div></div><span id="cascade-b2-beat-1"><<linkreplace "Take the keys.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> ...God, it's good to not be the only one anymore. Here — this is everything. Don't make me regret it.</div></div><p>He hands her the real access: the sealed side of the hunt, the run of it he's kept off every desk but his own. She's all the way in now — exactly where she needs to be.</p>[[Take it all.->Location_vance_securities]]
<</linkreplace>></span></span>
<<set $game_state.current_canvas = "cap_calloway_access">>
<<set $game_state.current_node = "cap_calloway_access.base">>
<<script>>setup.markCanvasTriggered("cap_calloway_access");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_calloway_access.base") === -1) { vn.push("cap_calloway_access.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "calloway_believes", "set");<</script>><p>A leaving card goes round in a manila envelope — third one this month. She signs under a dozen names she doesn't know, for a woman she's never met, packing a plant and a decade into one copier-paper box. Nobody's planning a party. There's nowhere left to have it.</p>
<<set $game_state.current_canvas = "amb_floor_goodbye">>
<<set $game_state.current_node = "amb_floor_goodbye.base">>
<<script>>setup.markCanvasTriggered("amb_floor_goodbye");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("amb_floor_goodbye.base") === -1) { vn.push("amb_floor_goodbye.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Pass it on.->Location_vance_securities]]<p>Two auditors in good suits are sealing the old records office with tamper tape and a printed inventory, working right past a name plate nobody's taken down. They don't look at her. To them the whole division is already a line item that closes Friday.</p>
<<set $game_state.current_canvas = "amb_floor_audit">>
<<set $game_state.current_node = "amb_floor_audit.base">>
<<script>>setup.markCanvasTriggered("amb_floor_audit");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("amb_floor_audit.base") === -1) { vn.push("amb_floor_audit.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Walk past.->Location_vance_securities]]<p>The lights over the far half of the floor have been off a week — nobody filed the ticket. A dead colleague's monitor still cycles a screensaver over a chair that isn't coming back, and the building keeps the heat on out of habit. It's very quiet in here for a place that's supposed to be working.</p>
<<set $game_state.current_canvas = "amb_floor_dying">>
<<set $game_state.current_node = "amb_floor_dying.base">>
<<script>>setup.markCanvasTriggered("amb_floor_dying");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("amb_floor_dying.base") === -1) { vn.push("amb_floor_dying.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Get back to it.->Location_vance_securities]]<p>She comes onto the floor out of the kit, back in her own sharp line, and Calloway catches it before she's three steps in. He knows her — that's not the problem. The problem is everyone else who knows a junior analyst doesn't dress like that.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> What are you doing up here looking like that. You want the audit asking who the analyst in the good coat is? Go. Change. Come back nobody.</div></div><p>He's right and they both know it. Nothing she needs off this floor happens while she's wearing the wrong face. The kit's back on the rack in her quarters.</p>
<<set $game_state.current_canvas = "react_calloway_nocover">>
<<set $game_state.current_node = "react_calloway_nocover.base">>
<<script>>setup.markCanvasTriggered("react_calloway_nocover");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("react_calloway_nocover.base") === -1) { vn.push("react_calloway_nocover.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Go change.->Location_vance_securities]]<img src="./videos/scenes/archive_cover.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She comes onto the Vance floor in her own face — the sharp company line, not the plain kit — and it lands wrong the second the light finds her. A junior analyst doesn't dress like she's worth more than the floor she's standing on. A head comes up. On a division being audited, a turned head is how a question starts.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> Wrong skin for this floor. Nothing gets done looking like this. The kit's on the rack up in her quarters — where it does her no good.</div></div>
<<set $game_state.current_canvas = "react_calloway_precover">>
<<set $game_state.current_node = "react_calloway_precover.base">>
<<script>>setup.markCanvasTriggered("react_calloway_precover");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("react_calloway_precover.base") === -1) { vn.push("react_calloway_precover.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = setup.pendingEffects || [];setup.addToWardrobe("cover_analyst");setup.showEffectNotification();<</script>>
[[Back out before it's a question.->Location_spire_plaza]]<p>Vane's at the coffee machine, waiting out a slow drip, easy the way the other three never quite manage. He makes room for her without being asked.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/vane.jpg" alt="Vane" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Vane:</strong> Don't let Reyes get to you. She's right that it's over and wrong about how to leave. You do the job clean to the last day — that's the one thing they can't take back. Calloway taught me that, funnily enough.</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> You believe him? The leak?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/vane.jpg" alt="Vane" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Vane:</strong> I believe there's always something moving that shouldn't be. Whether he ever gets his hands on it...</div></div><p>He shrugs, easy, and takes his coffee back to a desk tidier than anyone else's on a floor that's dying. The most trustworthy man in the room.</p>
<<set $game_state.current_canvas = "walkin_chat_vane">>
<<set $game_state.current_node = "walkin_chat_vane.base">>
<<script>>setup.markCanvasTriggered("walkin_chat_vane");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("walkin_chat_vane.base") === -1) { vn.push("walkin_chat_vane.base"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Let him get back to it.->Location_vance_securities]]<p>Marr's got the crossword folded to a quarter and a pen he isn't using. He raises it in a lazy salute.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/marr.jpg" alt="Marr" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Marr:</strong> The dream team. A woman who actually reads the files, a boss who believes in monsters, and me. You know the shredder's run more hours this month than the three of us combined? That's the real audit finding. Nobody writes it down.</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> You could help him.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/marr.jpg" alt="Marr" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Marr:</strong> I could. Six letters for 'why bother.'</div></div>
<<set $game_state.current_canvas = "walkin_chat_marr">>
<<set $game_state.current_node = "walkin_chat_marr.base">>
<<script>>setup.markCanvasTriggered("walkin_chat_marr");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("walkin_chat_marr.base") === -1) { vn.push("walkin_chat_marr.base"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Leave him his crossword.->Location_vance_securities]]<p>Reyes has her jacket on at her desk, phone face-up, one eye on the door out.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/reyes.jpg" alt="Reyes" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Reyes:</strong> Advice, since you're new and nobody else will: don't get pulled into the ghost story. There's no rogue. There's a sad man who missed his window and needs the leak to be real so the last two years meant something. I'm gone Friday. You should be looking too.</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> And if the leak's real?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/reyes.jpg" alt="Reyes" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Reyes:</strong> Then it's above our clearance and someone else's problem. Same difference to us.</div></div>
<<set $game_state.current_canvas = "walkin_chat_reyes">>
<<set $game_state.current_node = "walkin_chat_reyes.base">>
<<script>>setup.markCanvasTriggered("walkin_chat_reyes");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("walkin_chat_reyes.base") === -1) { vn.push("walkin_chat_reyes.base"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Let her watch the door.->Location_vance_securities]]<p>Voss is taping a box shut at the desk he's had since before she was built. He doesn't stop for her.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/voss.jpg" alt="Voss" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Voss:</strong> Thirty-one years. They give you a badge and a mug and tell you it means something, and one Tuesday a kid with a clipboard decides the whole floor's a rounding error. Calloway's the only one who ever gave a damn what was in these boxes.</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> He still does.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/voss.jpg" alt="Voss" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Voss:</strong> That's the tragedy of him, isn't it.</div></div>
<<set $game_state.current_canvas = "walkin_chat_voss">>
<<set $game_state.current_node = "walkin_chat_voss.base">>
<<script>>setup.markCanvasTriggered("walkin_chat_voss");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("walkin_chat_voss.base") === -1) { vn.push("walkin_chat_voss.base"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Leave him to his boxes.->Location_vance_securities]]<<set _sub_target = setup.checkAndSubstituteCanvas("chat_the_floor")>><<if _sub_target>><<goto _sub_target>><</if>>
<p>She takes a lap of the floor with a coffee she won't drink — the analyst's oldest cover, looking busy going nowhere. Half the desks are boxed up. The other half pretend the audit isn't three feet away.</p>
<<set $game_state.current_canvas = "chat_the_floor">>
<<set $game_state.current_node = "chat_the_floor.base">>
<<script>>setup.markCanvasTriggered("chat_the_floor");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("chat_the_floor.base") === -1) { vn.push("chat_the_floor.base"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Back to it.->Location_vance_securities]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "calloway_drains_done", "operator": "lt", "value": 1}]})>>
<p>She pulls a box he hasn't reached and starts cross-referencing the dates against the pulls he's flagged — the actual work, the part every other minder skipped straight past on the way to the close order.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> You're... actually reading them. Huh. Left column's the ones that went missing clean. Right's the ones somebody dressed up after. Don't mix them or I lose a week.</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Then I won't mix them.</div></div><p>He goes quiet and works beside her — not warming to her, exactly. Warming to not being alone in it.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "calloway_drains_done", "operator": "gte", "value": 1}]})>>
<p>She works the boxes beside him the same as before — except it's cover now, not courtship. He believes every inch of her; she's the one who decides what the case turns up, and when.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> Left column clean, right column dressed. You know the drill by now. Don't know what I did before you.</div></div><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> Nothing. He did nothing before her and he'll remember nothing after. She logs another hour at the desk of a man who's already hers.</div></div>
<</if>><p>She squares the pulled boxes back into the row and marks the hour on his sheet.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "case_insider_seen", "operator": "is_false"}]})>>
<p>Dates, pull-slips, signatures. The boxes stay boxes. Whatever's in here is in the pattern and not the pages — and the pattern needs more of them open than she's got open.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "case_insider_seen", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "case_curated_seen", "operator": "is_false"}]})>>
<p>She has the how now: nobody broke in. What she doesn't have is whose hand. That's in his two years of dead ends, not in the thefts — and there are a lot of dead ends left to read.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "case_curated_seen", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "calloway_believes", "operator": "is_false"}]})>>
<p>The trail runs into the part of the hunt he keeps sealed, and no box on this floor opens that. He does — when he decides she's worth handing it to.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "case_curated_seen", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "calloway_believes", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "pipe_known", "operator": "is_false"}]})>>
<p>The sealed side is hers now and the far end is close. A pass or two more through the logistics — the contracts, the manifests that don't match — and she'll have where it goes.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "pipe_known", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "colm_drains_done", "operator": "lt", "value": 1}]})>>
<p>The paper's finished. It gave up the how and it gave up where, and it will never give up a name — the man carrying it was never written down. That answer's down in the underworld, at the Undertow, not in this room.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "colm_drains_done", "operator": "gte", "value": 1}]})>>
<p>She has the rogue, and he's two desks away. Nothing in these boxes confirmed it and nothing in them ever will. What's left isn't filing. It's Vane.</p>
<</if>>
<<set $game_state.current_canvas = "work_the_case">>
<<set $game_state.current_node = "work_the_case.base">>
<<script>>setup.markCanvasTriggered("work_the_case");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("work_the_case.base") === -1) { vn.push("work_the_case.base"); }<</script>>
<<script>>advanceTime(60);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("npc", "npc_calloway", "relation", "add", 3.0, false, null);setup.applyAndNotifyTrait("player", null, "case_progress", "add", 1.0, false, null);setup.showEffectNotification();<</script>>
[[Log the hour.->Location_vance_securities]]<<set _sub_target = setup.checkAndSubstituteCanvas("hub_calloway_fileroom")>><<if _sub_target>><<goto _sub_target>><</if>>
<img src="./videos/scenes/calloway_fileroom.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Calloway's file room. Boxes to the ceiling, an index on none of them, a man who knows where everything is and trusts no one else to touch it. He works like the audit isn't three streets away with a shredder.</p>
<<set $game_state.current_canvas = "hub_calloway_fileroom">>
<<set $game_state.current_node = "hub_calloway_fileroom.base">>
<<script>>setup.markCanvasTriggered("hub_calloway_fileroom");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_calloway_fileroom.base") === -1) { vn.push("hub_calloway_fileroom.base"); }<</script>>
<<nobr>>
<<link "Talk to him." "Canvas_hub_calloway_fileroom_Node_talk">><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "calloway_believes", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_calloway", "trait_key": "corruption", "operator": "gte", "value": 0}, {"type": "trait", "subject": "player", "trait_key": "calloway_drains_done", "operator": "lt", "value": 1}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('hub_calloway_fileroom:cc0') ? '' : 'unlocked-choice'">
<<link "Get close to him." "Canvas_rung_calloway_contact_Node_base">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_calloway", "corruption", "add", 3.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_calloway", "arousal", "add", 3.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hub_calloway_fileroom:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Get close to him.">Get close to him. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "calloway_believes", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_calloway", "trait_key": "corruption", "operator": "gte", "value": 6}, {"type": "trait", "subject": "player", "trait_key": "calloway_drains_done", "operator": "lt", "value": 1}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('hub_calloway_fileroom:cc1') ? '' : 'unlocked-choice'">
<<link "Take him in your mouth." "Canvas_rung_calloway_oral_Node_base">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_calloway", "corruption", "add", 3.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_calloway", "arousal", "add", 4.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "calloway_oral_once", "set");<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hub_calloway_fileroom:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Take him in your mouth.">Take him in your mouth. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "calloway_believes", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "calloway_flinch_resolved", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_calloway", "trait_key": "corruption", "operator": "gte", "value": 18}]})>>
<span @class="setup.isChoiceVisited('hub_calloway_fileroom:cc2') ? '' : 'unlocked-choice'">
<<link "Take him — all the way." "Canvas_loop_calloway_sex_Node_intro">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "calloway_fucked_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hub_calloway_fileroom:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Not yet — he has to trust you all the way down first.">Not yet — he has to trust you all the way down first.</span><br>
<</if>>
<<link "Leave him to the paper." "Location_vance_securities">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "calloway_drains_done", "operator": "lt", "value": 1}]})>>
<div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Walk me through the ghost. From the start — the parts nobody let you finish.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> ...Fine. You keep up, and you keep it in this room. I've had enough people carry half of it out and call me paranoid.</div></div>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "calloway_drains_done", "operator": "gte", "value": 1}]})>>
<div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Anything new on the ghost?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> For you? Everything I've got. Don't know what I ran this on before you walked in.</div></div><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> He hands it over before she's done asking. He'll never know the file room's been hers since the night his own current gave up where the vault is.</div></div>
<</if>>
<<set $game_state.current_canvas = "hub_calloway_fileroom">>
<<set $game_state.current_node = "hub_calloway_fileroom.talk">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_calloway_fileroom.talk") === -1) { vn.push("hub_calloway_fileroom.talk"); }<</script>>
<<script>>advanceTime(60);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("npc", "npc_calloway", "relation", "add", 2.0, false, null);setup.showEffectNotification();<</script>>
[[Keep at it.->Location_vance_securities]]<img src="./videos/scenes/calloway_meet.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>He's at the file wall when she comes in, and he doesn't turn — just clocks the badge in the dark glass and files her under the same heading as everyone else Mercer's sent down here.</p><span id="cascade-b2-beat-0"><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> Nobody reads the file. They read the summary, sign the close order, and go to lunch. That what you're here to do — sign it?</div></div><p>He says it the way he's said it a hundred times, to a hundred minders.</p><span id="cascade-b2-beat-1"><<linkreplace "Take the hunt seriously.">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> The rogue's real. You're the only one still looking. That's the file I'd want to read.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/calloway.jpg" alt="Calloway" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Calloway:</strong> ...Huh. They usually don't bother learning the words. Sit down. Don't touch anything.</div></div>[[Get to work.->Location_vance_securities]]
<</linkreplace>></span></span>
<<set $game_state.current_canvas = "cap_calloway_meet">>
<<set $game_state.current_node = "cap_calloway_meet.base">>
<<script>>setup.markCanvasTriggered("cap_calloway_meet");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_calloway_meet.base") === -1) { vn.push("cap_calloway_meet.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "calloway_met", "set");<</script>><img src="./videos/scenes/archive_placement.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The badge desk scans her lanyard without looking up and waves her at an empty chair — one of a dozen, most already boxed. She's on Calloway's division now, or what the audit's left of it.</p><p>The woman at the next desk clocks the new badge and doesn't bother pretending she's working.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/reyes.jpg" alt="Reyes" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Reyes:</strong> Nobody told you what you walked into. Fine — since I'm the only one who's going to.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/reyes.jpg" alt="Reyes" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Reyes:</strong> Voss over there's done thirty years and still thinks Calloway hung the moon. Marr does the crossword — that's the whole job now. Vane you'll barely notice, keeps his head down, does it clean. And me, I'm gone the second something better calls back.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/reyes.jpg" alt="Reyes" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Reyes:</strong> Advice? Answer the auditors, not him. Calloway's chasing a ghost, and half of us think the ghost is just him coming apart. Don't catch it.</div></div><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> Four people running out the clock on a dead floor. To them she's one more temp badge, gone before the boxes are. Exactly what the cover needs — no one worth remembering.</div></div>
<<set $game_state.current_canvas = "cap_archive_placement">>
<<set $game_state.current_node = "cap_archive_placement.base">>
<<script>>setup.markCanvasTriggered("cap_archive_placement");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_archive_placement.base") === -1) { vn.push("cap_archive_placement.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "floor_placed", "set");<</script>>
[[Find the file room.->Location_vance_securities]]<img src="./videos/scenes/archive_cover.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The plain kit holds all the way up — the junior-analyst badge Mercer bought clean, the grays that make her no one worth a second look, a desk in a division being quietly shut down. The lift opens on Vance Securities: half the lights off, half the desks empty, the audit already eating it from inside.</p><p>The file room is Calloway's. He runs the company's hunt for the rogue out of it, and the floor treats him like a crank chasing a ghost. The paper he keeps has no index — the only map of it lives in his head.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> A plain company errand, the way Mercer put it. Get in, take the room, bring it back. She doesn't turn over what might sit under a job this simple. First she needs the man who keeps it.</div></div>
<<set $game_state.current_canvas = "cap_archive_cover">>
<<set $game_state.current_node = "cap_archive_cover.base">>
<<script>>setup.markCanvasTriggered("cap_archive_cover");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_archive_cover.base") === -1) { vn.push("cap_archive_cover.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "archive_started", "set");<</script>>
[[Get to work.->Location_vance_securities]]<img src="./videos/scenes/salvage_relaunch.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She comes up the ramp into the grey morning, and the comm in her jaw clicks live before she's ten steps clear — Mercer, who always knows the minute she's operational again.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> You went dark. Days. I don't like dark, and I like even less what walks back out of it. Who put hands on you.</div></div><span id="cascade-b3-beat-0"><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> A mechanic. Off the books. I'm operational.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> A mechanic. You let some dockside butcher open you up and go rooting around in company property. You have any idea what you're worth. What's in you.</div></div><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> More than you'll ever say out loud. I've met the part you keep the key to.</div></div><span id="cascade-b3-beat-1"><<linkreplace "Take the order.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> New target. Calloway — Vance Securities, internal division. He runs the hunt for the rogue from the inside, and he keeps a file room nobody audits. Get in. Bring me what's in it.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> There's a junior-analyst kit in your quarters. Wear it onto that floor — badge, plain grays, the whole nothing look. Not your own face. A division under audit remembers anyone who looks worth remembering.</div></div><span id="cascade-b3-beat-2"><<linkreplace "Say yes.">><p>She says yes the way she says yes to everything. But it sits on a new floor now. Every order he gives her, he gives holding the one key that can cut the thing at the root of her — and he's never once had to say so, because until three days ago she didn't know it was there. Now she does. It doesn't change the yes. It changes what she's saying yes for.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> You still in one piece?</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Operational.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Then move. And Wren — next time you break, you break to me. Not to some yard rat. What's mine gets fixed by mine. Understand.</div></div>[[Go to work.->Location_the_waterfront]]
<</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "salvage_relaunch">>
<<set $game_state.current_node = "salvage_relaunch.base">>
<<script>>setup.markCanvasTriggered("salvage_relaunch");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_relaunch.base") === -1) { vn.push("salvage_relaunch.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "salvage_relaunched", "set");<</script>>
<<script>>setup.pendingEffects = setup.pendingEffects || [];setup.addToWardrobe("cover_analyst");setup.showEffectNotification();<</script>><p>The lift opens on the penthouse and Mercer looks up from the glass wall — and stops. Whatever he keeps her for goes out of his face and something colder comes in. He crosses the floor and takes her jaw in one hand, turning her face to the light like a man checking a coin for a file-mark.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> What did they do to you.</div></div><span id="cascade-b2-beat-0"><p>She doesn't answer. There isn't an answer that helps her. He turns her head the other way, reading the wrongness off her the way she reads it off a man — except he had the building of her, and he knows the manual.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> You go dark for days, you come back moving like that, the core reading wrong through your skin. No. An asset that limps is a liability, and I don't hand the Chairman a liability.</div></div><span id="cascade-b2-beat-1"><<linkreplace "Hear the refusal.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> You get nothing from me like this. No target, no work, nothing — I'm not spending you broken. Go get yourself right, then come back and work. Not before.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Now get out of my sight.</div></div><span id="cascade-b2-beat-2"><<linkreplace "Go.">><p>The lift takes her back down. She has her answer, and it's the one the dock already gave her: there's no working her way back in until she can fire. Which means the berth. Which means coin. Which means down the gate, where coin gets made.</p>[[Back down.->Location_the_waterfront]]
<</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "salvage_mercer_shocked">>
<<set $game_state.current_node = "salvage_mercer_shocked.base">>
<<script>>setup.markCanvasTriggered("salvage_mercer_shocked");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_mercer_shocked.base") === -1) { vn.push("salvage_mercer_shocked.base"); }<</script>>
<<script>>advanceTime(3);<</script>><img src="./videos/scenes/salvage_verdict.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess closes the last panel and steps back, wiping his hands, and looks at Wren the way he'd look at a hull he's decided is finished — not fixed. Finished.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> That's you operational. Leg holds, drain fires clean, core takes a charge without cooking itself. You're a weapon again. Congratulations.</div></div><span id="cascade-b3-beat-0"><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> But I told you there was something down there I didn't like. I had my proper look while I had you open. And here's the thing — it's not damage. Everything else they broke or wore out. This they built.</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Built what.</div></div><span id="cascade-b3-beat-1"><<linkreplace "Hear it.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> A governor. A bad-tempered little piece of hardware seated under your core, older than the rest of you, wired so deep it's part of the frame. It's not there to run you. It's there to stop you — one specific thing you're built so you cannot do.</div></div><span id="cascade-b3-beat-2"><<linkreplace "What thing.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There's a man you can't point your teeth at. You read every other body I put on this cradle by the end — but the one who owns you, you couldn't drain him if he was open on this table in front of you. That's not you being loyal. That's not you being scared. That's this.</div></div><span id="cascade-b3-beat-3"><<linkreplace "Take it out, then.">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Then take it out.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Can't. It's keyed — pull it blind and it takes you with it, and I don't do wakes. There's a controller for it somewhere. A key. Whoever holds it cuts it clean in a minute. I don't have it. You don't. Not my business whose it is.</div></div><span id="cascade-b3-beat-4"><<linkreplace "You already know whose.">><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> He signs for me. He's held the key the whole time.</div></div><p>She doesn't ask him whose. She already has the shape of the answer, and it's the shape of the man whose name is on her papers — the one who's spent every order she ever took knowing she could never turn and make him stop.</p><span id="cascade-b3-beat-5"><<linkreplace "Feel it settle.">><p>Something settles into place at the root of her that isn't comfort. She can feel exactly where it sits now — a locked thing, one door, and no key on her side of it. She'll carry it. A thing she can see is a thing she can carry.</p><span id="cascade-b3-beat-6"><<linkreplace "Settle up.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> We're square — you paid as you went, most don't. Go on. You've got a key to find.</div></div><span id="cascade-b3-beat-7"><<linkreplace "Stand up.">><p>She stands off the cradle. The leg holds, the drain sits true. For the first time she knows the exact shape of the thing that's owned her — not a feeling, a component, with a key, and the key is somewhere she can walk to and take. That isn't despair. It's a map.</p><span id="cascade-b3-beat-8"><<linkreplace "And under that.">><p>And under that, further down, the name from the leak — the one syllable that fit her like a socket and then slipped. She still can't hold it. She's just stopped being able to stop reaching for it.</p>[[Go to work.->Location_the_waterfront]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "salvage_verdict">>
<<set $game_state.current_node = "salvage_verdict.base">>
<<script>>setup.markCanvasTriggered("salvage_verdict");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_verdict.base") === -1) { vn.push("salvage_verdict.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "core_sealed", "set", 1.0, false, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "salvage_done", "set");setup.applyAndNotifyFlag("player", null, "mission_3_active", "set");<</script>><p>Wren's off the cradle, catching her breath, when Kess stops with his hand still in the open panel at the base of her spine. He's gone still the way a man goes still when the wrench finds a bolt that shouldn't be there.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Hold on.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There's something down here I don't like. Wired deeper than the rest of you — under the core, not in it. Older, too. It's not what's stopping the drain, so I've been working around it. But I want a proper look before I close you up for good.</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> What is it?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Don't know yet. That's what I don't like. Finish getting your teeth back, then we'll talk about it.</div></div>
<<set $game_state.current_canvas = "salvage_chip_snag">>
<<set $game_state.current_node = "salvage_chip_snag.base">>
<<script>>setup.markCanvasTriggered("salvage_chip_snag");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_chip_snag.base") === -1) { vn.push("salvage_chip_snag.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Later, then.->Location_kess_berth]]<img src="./videos/scenes/salvage_session_12.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess has a last body waiting — a dock-hand, paid, unbothered. He doesn't look worried this time. The readout in his hand is already half-green before Wren's even crossed the floor.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Core's warm. Warmer than it's ever read on my bench. This is the last one — I just need to see it hold under load. So turn it over, use him, and reach for the take at the finish.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> One clean pull and you're done. Don't fight it this time. Let it work.</div></div>
<<set $game_state.current_canvas = "salvage_session_12">>
<<set $game_state.current_node = "salvage_session_12.base">>
<<script>>setup.markCanvasTriggered("salvage_session_12");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_12.base") === -1) { vn.push("salvage_session_12.base"); }<</script>>
<<nobr>>
<<link "Get to it." "Canvas_salvage_session_12_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_12_fuck.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>The dock-hand's efficient — bends her over the cradle, takes her from behind, sets a steady pace and holds it. "Strangest job I've had," he says over his shoulder, half a laugh. Kess doesn't answer. He's watching the needle sit flat and green.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Steady. It's not even flickering. Take him to the finish.</div></div>
<<set $game_state.current_canvas = "salvage_session_12">>
<<set $game_state.current_node = "salvage_session_12.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_12.n2") === -1) { vn.push("salvage_session_12.n2"); }<</script>>
<<nobr>>
<<link "Reach for the take." "Canvas_salvage_session_12_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_12_finish.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He finishes on her and she reaches — the old pull, down at the base of the spine. No cold socket this time, no clawing after a dead thing. His secrets walk out of him in one clean draw. It opens easy; the take barely costs her a want.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There. Full pull, right through the finish, no strain on the readout at all. That's whole. That's a clean core.</div></div><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}]})>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Core's trim now, holding under load. So — the other one, last time. Put it on him and mean it.</div></div><p>She levels the emitter at the dock-hand and keys it. The field rolls out full and even and drops over him like weather. He goes slack where he stands, eyes gone, a dead weight held up by nothing — under, and staying under.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> That's it. Full field, clean drop. Both weapons live off one good core, and it's good now. You came in here with a dead drain and a dead emitter. You walk out with two.</div></div>
<</if>>
<<set $game_state.current_canvas = "salvage_session_12">>
<<set $game_state.current_node = "salvage_session_12.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_12.n3") === -1) { vn.push("salvage_session_12.n3"); }<</script>>
<<nobr>>
<<link "Let him go." "Canvas_salvage_session_12_Node_read">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The dock-hand dresses, pockets his coin, lets himself out into the dark. Kess sets the readout down and doesn't reach for a probe. Nothing left to chase down the seam.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> You're done. I mean it — that's the last visit. The drive's back, the drain fires clean, and it held under load without a wobble. Operational.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Whatever they broke in you, it's mended. Get off my cradle and go find out what you can do with it.</div></div>
<<set $game_state.current_canvas = "salvage_session_12">>
<<set $game_state.current_node = "salvage_session_12.read">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_12.read") === -1) { vn.push("salvage_session_12.read"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", -8.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_session", "add", 1.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 0.0, true, null);setup.applyAndNotifyTrait("player", null, "emitter_broken", "set", 0.0, true, null);setup.showEffectNotification();<</script>>
[[Off the cradle. Whole.->Location_kess_berth]]<img src="./videos/scenes/salvage_session_11.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess has a body waiting — a dockside regular she's used before, paid up front, unbothered. He knows the shape of the job by now. Stands loose against the bench, waiting to be told nothing.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Core's warm today. Warmer than it's been. So don't be careful with it — take him how you want him, top to bottom, all your call. I want the drive under load, not idling.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> And reach for the take at the finish. Every time. I think this one gives you something back.</div></div>
<<set $game_state.current_canvas = "salvage_session_11">>
<<set $game_state.current_node = "salvage_session_11.base">>
<<script>>setup.markCanvasTriggered("salvage_session_11");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_11.base") === -1) { vn.push("salvage_session_11.base"); }<</script>>
<<nobr>>
<<link "Take him." "Canvas_salvage_session_11_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_11_fuck.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She puts him down on the cradle and climbs on, sets the pace off her own hips — slow, then flat and hard, exactly the rhythm she wants and not his. "Fine by me," he says, hands off, letting her. Kess watches the meter climb.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> That's it. Needle's moving — real charge this time. Don't stop, don't ease it. Run him.</div></div>
<<set $game_state.current_canvas = "salvage_session_11">>
<<set $game_state.current_node = "salvage_session_11.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_11.n2") === -1) { vn.push("salvage_session_11.n2"); }<</script>>
<<nobr>>
<<link "Reach for the take." "Canvas_salvage_session_11_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_11_finish.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She wrings him dry and reaches — down inside for the old pull that used to open a man at the base of the spine. This time it catches. It fires clean, full take, and the whole shape of him lays open in her hands.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There it is. Full pull, clean through the finish, no sputter — that's the weapon. That's you. Welcome back.</div></div><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}]})>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Drain's clean now. Try the other one again — same as before, thumb it at him.</div></div><p>She keys the emitter at the regular. This time a field rolls off it, thin and stuttering. His eyes glass, his breath drags, his knees dip — then it guts out and he catches the bench, swaying, still up on his feet.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There — it drove that time. Weak, half a field, didn't put him down. But the core spilled into the second draw. Can't hold it yet. One more session and it lands full.</div></div>
<</if>>
<<set $game_state.current_canvas = "salvage_session_11">>
<<set $game_state.current_node = "salvage_session_11.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_11.n3") === -1) { vn.push("salvage_session_11.n3"); }<</script>>
<<nobr>>
<<link "Let him go." "Canvas_salvage_session_11_Node_read">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The regular dresses and lets himself out, emptied and none the wiser, laid open and already forgetting it. Kess kills the readout and turns it around so Wren can see the line spike and hold.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Drive's seated. Drain's clean. Whatever seized in you, it's turning over on its own now — I don't have to nurse it anymore.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> You came in here a wreck with a cold socket. You're walking out a weapon. Go use it on someone who matters.</div></div>
<<set $game_state.current_canvas = "salvage_session_11">>
<<set $game_state.current_node = "salvage_session_11.read">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_11.read") === -1) { vn.push("salvage_session_11.read"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", -8.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_session", "add", 1.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 0.0, true, null);setup.showEffectNotification();<</script>>
[[Off the cradle. Armed.->Location_kess_berth]]<img src="./videos/scenes/salvage_session_10.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess has a body waiting — a big one this time, a dock-hand off the heavy cranes, broad and slow-moving. Paid, told nothing. He stands where he's put and says less, patient the way big men are.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Core's warm now. Last session it caught for a breath — this one it'll catch and hold, I think. So turn it over. Bend him to it, take him deep, work him proper.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> And reach for the take at the finish. Every time. I want to see how much of him you can pull.</div></div>
<<set $game_state.current_canvas = "salvage_session_10">>
<<set $game_state.current_node = "salvage_session_10.base">>
<<script>>setup.markCanvasTriggered("salvage_session_10");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_10.base") === -1) { vn.push("salvage_session_10.base"); }<</script>>
<<nobr>>
<<link "Get to it." "Canvas_salvage_session_10_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_10_fuck.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She bends him to it — takes him over the cradle, gets herself braced and open, and he feeds his cock into her cunt deep and unhurried, all that weight behind each stroke. "Tell me if it's too much," he says, low. She won't. Kess watches the meter.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Needle's climbing — good, that's the core drinking. Keep him deep, keep the seat full. Don't rush the stroke; the drain wants the whole finish, not a fast one. Let it build.</div></div>
<<set $game_state.current_canvas = "salvage_session_10">>
<<set $game_state.current_node = "salvage_session_10.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_10.n2") === -1) { vn.push("salvage_session_10.n2"); }<</script>>
<<nobr>>
<<link "Reach for the take." "Canvas_salvage_session_10_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_10_finish.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He finishes deep, spills up into her cunt, a long shudder of all that weight. She reaches down inside for the old pull, and this time it catches. Grabs.</p><p>The socket floods and takes. The shape of the man comes up her spine near whole — his years, his ledger, his quiet. Most of him, in one long draw.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There — there it is. Strong catch, socket flooding, meter near the top. That's nearly the full take; almost the whole man pulled off one finish. His years, his weight, all of it up the trace. That's the readout I wanted.</div></div><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}]})>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> While he's still standing — that emitter of yours runs off this same core. If the drain's catching, the other one might wake too. Thumb it at him. Let's see.</div></div><p>She pulls the emitter, levels it at the spent dock-hand, and keys it. Nothing rolls off it — no field, no heat, no give. The big man just blinks at her, waiting. Dead weight in her hand.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Nothing. Figured as much. Core's barely driving the one draw — it's got nothing spare for a second yet. Leave it. It'll come back when the drain's all the way trim.</div></div>
<</if>>
<<set $game_state.current_canvas = "salvage_session_10">>
<<set $game_state.current_node = "salvage_session_10.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_10.n3") === -1) { vn.push("salvage_session_10.n3"); }<</script>>
<<nobr>>
<<link "Let him go." "Canvas_salvage_session_10_Node_read">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The big one dresses slow and lets himself out, paid twice and lighter than he came, though he'll never know it. Kess pulls the readout and runs a thumb down the trace, nodding at what he sees.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Drive's nearly back. It caught strong and held near the whole way — that's a working weapon, just short of trim. You're one session from operational. Off, back, and this next one seals it.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Come back when you've got coin for the last. One more finish and you walk out of here armed.</div></div>
<<set $game_state.current_canvas = "salvage_session_10">>
<<set $game_state.current_node = "salvage_session_10.read">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_10.read") === -1) { vn.push("salvage_session_10.read"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", -8.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_session", "add", 1.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 0.0, true, null);setup.showEffectNotification();<</script>>
[[Off the cradle — one to go.->Location_kess_berth]]<img src="./videos/scenes/salvage_session_9.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess has a body waiting when Wren comes in — a talker, dockside, paid up front and already three sentences deep about the weather, the boats, his cousin's boat. The kind of man who fills a room to keep from thinking.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> This one runs his mouth. Good. The drain likes a man who's already leaking — easier to catch the thread and pull it. Core's warmer now. I think it'll hold this time.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> So turn it over, use him however you like — and reach for the take at the finish. If it catches, don't fight it. Let it walk a whole sentence out of him.</div></div>
<<set $game_state.current_canvas = "salvage_session_9">>
<<set $game_state.current_node = "salvage_session_9.base">>
<<script>>setup.markCanvasTriggered("salvage_session_9");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_9.base") === -1) { vn.push("salvage_session_9.base"); }<</script>>
<<nobr>>
<<link "Get to it." "Canvas_salvage_session_9_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_9_fuck.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She leans back on the cradle and puts him where she wants him, and for once the mouth goes quiet — he works her cunt with his tongue, slow, hands spread on her thighs.</p><p>"Quiet one," he mumbles up at her between passes, half a laugh. "That's fair. Fair." Kess watches the meter, not them.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There — needle's twitching already. He's open. Keep him going. I want him spent when you reach for it.</div></div>
<<set $game_state.current_canvas = "salvage_session_9">>
<<set $game_state.current_node = "salvage_session_9.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_9.n2") === -1) { vn.push("salvage_session_9.n2"); }<</script>>
<<nobr>>
<<link "Take him inside." "Canvas_salvage_session_9_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_9_finish.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She pulls his cock up into her cunt and rides it out, and when he empties she reaches — the old pull, down at the base of his spine — and this time it grabs. Holds.</p><p>A word comes up through him and out his mouth like it was hers to take: "Nobody knows I sank the Marda. I cut her loose myself. For the coin." Then the thread drops.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There it is. Held right through the finish — a whole sentence, clean, true. He didn't even feel it leave. Good. That's the drain working.</div></div>
<<set $game_state.current_canvas = "salvage_session_9">>
<<set $game_state.current_node = "salvage_session_9.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_9.n3") === -1) { vn.push("salvage_session_9.n3"); }<</script>>
<<nobr>>
<<link "Let him go." "Canvas_salvage_session_9_Node_read">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The talker dresses, running his mouth again — the weather, the boats — never once wondering why he told a stranger the worst thing he'd ever done. He lets himself out. Kess is already behind her with the probe, reading the seam.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> That's the best pull yet. A full sentence, held clean through — the drive's nearly seated. Nearly there.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> One or two more and it'll hold on its own, no coaxing. Come back when you've got coin for the next.</div></div>
<<set $game_state.current_canvas = "salvage_session_9">>
<<set $game_state.current_node = "salvage_session_9.read">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_9.read") === -1) { vn.push("salvage_session_9.read"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", -8.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_session", "add", 1.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 0.0, true, null);setup.showEffectNotification();<</script>>
[[Off the cradle.->Location_kess_berth]]<img src="./videos/scenes/salvage_session_8.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess has a mark waiting — a dockside john she pulled off the coin-boards, shirt already off, sitting back on the bench like he owns the hour. He doesn't. He's meat with a pulse and a day in his head.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Core's warmer than last time. It'll turn over now — might even bite. Get on top of him. Run it your way, not his. You've done the lying-back work; I want you driving.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> And reach for the take at the finish. Every time. This one I think you catch.</div></div>
<<set $game_state.current_canvas = "salvage_session_8">>
<<set $game_state.current_node = "salvage_session_8.base">>
<<script>>setup.markCanvasTriggered("salvage_session_8");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_8.base") === -1) { vn.push("salvage_session_8.base"); }<</script>>
<<nobr>>
<<link "Get to it." "Canvas_salvage_session_8_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_8_fuck.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She puts him on his back and climbs on, sets the pace herself — slow, then hard when she wants it hard, hands flat on his chest to keep him down. "Strangest job on the coin-boards," he breathes, half-laughing. She doesn't answer. Kess watches the meter climb.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There it is — needle's live. Keep him going. Don't reach yet. Wait for the finish.</div></div>
<<set $game_state.current_canvas = "salvage_session_8">>
<<set $game_state.current_node = "salvage_session_8.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_8.n2") === -1) { vn.push("salvage_session_8.n2"); }<</script>>
<<nobr>>
<<link "Reach for the take." "Canvas_salvage_session_8_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_8_finish.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He empties under her and she reaches — down inside for the old pull, the thing that used to open a man and walk his secrets out. It catches. Weak, a flicker, but it grabs.</p><p>One thread of him comes loose — a wharf name, a crate he moved that morning, the low door behind it. Then it slips and guts out. Gone. But it fired.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> That — there. You felt that? It caught. Only a thread, only a scrap of his day, then it dropped. Doesn't matter. It fired. That's the first real take.</div></div>
<<set $game_state.current_canvas = "salvage_session_8">>
<<set $game_state.current_node = "salvage_session_8.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_8.n3") === -1) { vn.push("salvage_session_8.n3"); }<</script>>
<<nobr>>
<<link "Let him go." "Canvas_salvage_session_8_Node_read">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The mark dresses, tosses her a look he can't name, and lets himself out into the dock-dark none the wiser he was read. Kess is already at the meter, thumbing back the spike, reading the shape of the catch.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Weak, yeah. Grabbed a scrap and lost the rest. But hear me — that wasn't a memory of the weapon. That was the weapon. The thing itself, live, doing its work. Everything before this was a dead socket. This wasn't.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> It holds longer each time we run it. Costs you every visit. Come back with coin and we'll widen the catch — turn a thread into a whole man.</div></div>
<<set $game_state.current_canvas = "salvage_session_8">>
<<set $game_state.current_node = "salvage_session_8.read">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_8.read") === -1) { vn.push("salvage_session_8.read"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", -8.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_session", "add", 1.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 0.0, true, null);setup.showEffectNotification();<</script>>
[[Off the cradle.->Location_kess_berth]]<img src="./videos/scenes/salvage_session_7.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess has the next one waiting — a hand off the freighters, weathered, silent, paid up front. He doesn't ask what the job is. Men who work boats that long stop asking.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Core's warmer than last time. You'll feel it try. It won't hold, but it'll try — and that's the part I need to watch. So turn it over. Use him however you like.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> And reach for the take at the finish. Push it this time. Don't be gentle with it.</div></div>
<<set $game_state.current_canvas = "salvage_session_7">>
<<set $game_state.current_node = "salvage_session_7.base">>
<<script>>setup.markCanvasTriggered("salvage_session_7");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_7.base") === -1) { vn.push("salvage_session_7.base"); }<</script>>
<<nobr>>
<<link "Get to it." "Canvas_salvage_session_7_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_7_fuck.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He turns her, bends her over the bench, and works into her hard — no talk, no easing in, just a flat steady pound like he's done this his whole life. "Job's a job," he grunts once, and that's the only thing he says. Kess watches the readout, not them.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There. Needle's twitching. It knows he's there — it just can't get a grip yet.</div></div>
<<set $game_state.current_canvas = "salvage_session_7">>
<<set $game_state.current_node = "salvage_session_7.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_7.n2") === -1) { vn.push("salvage_session_7.n2"); }<</script>>
<<nobr>>
<<link "Reach for the take." "Canvas_salvage_session_7_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_7_finish.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He finishes deep and she reaches — hard, furious, willing the old pull to catch. It flickers. Something at the base of the spine stirs, the weapon coughing awake half a second. She drags at it and it dies anyway. Nothing walks out of him.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Did you feel that? It lit. Half a beat, then it dropped. That's not dead — that's a bad connection.</div></div>
<<set $game_state.current_canvas = "salvage_session_7">>
<<set $game_state.current_node = "salvage_session_7.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_7.n3") === -1) { vn.push("salvage_session_7.n3"); }<</script>>
<<nobr>>
<<link "Let him go." "Canvas_salvage_session_7_Node_read">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The hand pulls his trousers up and lets himself out, silent as he came in. Kess is already down behind her with the probe, chasing the flicker back along the seam, mouth tight.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> You were angry. Good. That's what pushed it over the line far enough to light. Anger's fuel — don't apologize for it, use it.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> It sputtered because the driver's still weak, not because it's gone. We're close. Two, maybe three more and it holds through the finish. Come back with coin for the next.</div></div>
<<set $game_state.current_canvas = "salvage_session_7">>
<<set $game_state.current_node = "salvage_session_7.read">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_7.read") === -1) { vn.push("salvage_session_7.read"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", -8.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_session", "add", 1.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 0.0, true, null);setup.showEffectNotification();<</script>>
[[Off the cradle.->Location_kess_berth]]<img src="./videos/scenes/salvage_session_6.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess has a body waiting when Wren comes in — younger than the others, quiet, first time by the look of him. Sitting too straight, hands flat on his knees, waiting to be told what to do.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> This one's green. He'll freeze if you leave it to him, so don't. Take him where you want him — slow to start, then hard. The drain's still faulting, but it caught a flicker last time. I want to see if it does it again.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> And reach for the take at the finish. Same as always. Reach whether it comes or not.</div></div>
<<set $game_state.current_canvas = "salvage_session_6">>
<<set $game_state.current_node = "salvage_session_6.base">>
<<script>>setup.markCanvasTriggered("salvage_session_6");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_6.base") === -1) { vn.push("salvage_session_6.base"); }<</script>>
<<nobr>>
<<link "Get to it." "Canvas_salvage_session_6_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_6_fuck.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She takes it off his hands — puts him on his back, works him in slow until the shake leaves him, then drops the pace and rides him hard. "Sorry — is that — am I doing it right," he manages, half a question. Kess doesn't look up from the readout.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> You're fine. Stop talking. There — needle just twitched. Keep her on him.</div></div>
<<set $game_state.current_canvas = "salvage_session_6">>
<<set $game_state.current_node = "salvage_session_6.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_6.n2") === -1) { vn.push("salvage_session_6.n2"); }<</script>>
<<nobr>>
<<link "Reach for the take." "Canvas_salvage_session_6_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_6_finish.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He finishes fast, gasping, hips jerking up into her — and she reaches down inside herself for the old pull. This time something answers.</p><p>A thin thread of it opens at the base of his spine, holds a half-beat, sputters like a wet match — then dies. But it held. Longer than nothing.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There. You feel that? It caught and ran a half-beat before it dropped. Last time was dead flat. That's not dead anymore.</div></div>
<<set $game_state.current_canvas = "salvage_session_6">>
<<set $game_state.current_node = "salvage_session_6.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_6.n3") === -1) { vn.push("salvage_session_6.n3"); }<</script>>
<<nobr>>
<<link "Let him go." "Canvas_salvage_session_6_Node_read">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The boy dresses in a hurry, red to the ears, and lets himself out. Kess pulls a stool up behind Wren and sets the probe to the seam, reading the trace it left.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> The drive's coming back. It held longer that time because it's remembering how — the pathway's still there, just slow to open. Each pass it'll hold a little more before it gives.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Don't chase it hard, you'll tear the seam. Rest, let the strain drop, come back when you've coin for the next. We build it in half-beats.</div></div>
<<set $game_state.current_canvas = "salvage_session_6">>
<<set $game_state.current_node = "salvage_session_6.read">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_6.read") === -1) { vn.push("salvage_session_6.read"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", -8.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_session", "add", 1.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 0.0, true, null);setup.showEffectNotification();<</script>>
[[Off the cradle.->Location_kess_berth]]<img src="./videos/scenes/salvage_session_5.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess has a body waiting when Wren comes in — a courier off the fast boats, young, paid, mouthy about it. Jacket half off, grinning like he's already got the story he'll tell later.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> This one's a talker. Ignore it. Core's warmer than last time — you might get a flicker off him, might get nothing. That's what I'm here to see. So turn it over, use him however you like.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> And reach for the take at the finish. Every time. Whatever comes up, I want the reading.</div></div>
<<set $game_state.current_canvas = "salvage_session_5">>
<<set $game_state.current_node = "salvage_session_5.base">>
<<script>>setup.markCanvasTriggered("salvage_session_5");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_5.base") === -1) { vn.push("salvage_session_5.base"); }<</script>>
<<nobr>>
<<link "Get to it." "Canvas_salvage_session_5_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_5_fuck.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>The courier's got no patience in him — spins her, bends her over the cradle, works into her hard and fast with his hands dug into her hips. "Best coin on the whole run," he says, breathless, cocky. "Gonna be back for this." Kess watches the meter, not them.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Save your breath, courier. The reading doesn't care how you feel about it.</div></div>
<<set $game_state.current_canvas = "salvage_session_5">>
<<set $game_state.current_node = "salvage_session_5.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_5.n2") === -1) { vn.push("salvage_session_5.n2"); }<</script>>
<<nobr>>
<<link "Reach for the take." "Canvas_salvage_session_5_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_5_finish.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He finishes hard inside her and she reaches down for the old pull — and this time something answers. A flicker. The socket warms, half-catches, a fraction of the take opening at the base of his spine.</p><p>Then it drops. Gone before it's a thing. She felt it almost work — worse than nothing.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There. There — it caught. Half a second, right through the finish, then it dropped. Did you feel that? That's not nothing. That's the first real one.</div></div>
<<set $game_state.current_canvas = "salvage_session_5">>
<<set $game_state.current_node = "salvage_session_5.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_5.n3") === -1) { vn.push("salvage_session_5.n3"); }<</script>>
<<nobr>>
<<link "Let him go." "Canvas_salvage_session_5_Node_read">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The courier dresses, still grinning, and lets himself out with a line about coming back. Kess ignores him, already crouched behind Wren with the probe, chasing that half-second flicker down the seam before it goes cold.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> It caught and it dropped — that's a driver waking up, not a dead one. The pull's still there. It just can't hold the charge long enough to walk anything out. Yet.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Feeling it half-work is the worst part. I know. But that flicker's the map back. Come back with coin and we chase it — next one, it holds longer.</div></div>
<<set $game_state.current_canvas = "salvage_session_5">>
<<set $game_state.current_node = "salvage_session_5.read">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_5.read") === -1) { vn.push("salvage_session_5.read"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", -8.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_session", "add", 1.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 0.0, true, null);setup.showEffectNotification();<</script>>
[[Off the cradle.->Location_kess_berth]]<img src="./videos/scenes/salvage_session_4.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess has a body waiting when Wren comes in — an old regular of his, already stripped, already on the cradle. Hands behind his head. He's done this before and it shows. Bored, easy, patient.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> This one's a regular. Knows the drill, won't spook, won't ask. Core's still cold — you'll pull nothing off him. But I want it turning over so I can read the driver.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> So turn it over. Use him however you like — ride him, finish him. And reach for the take at the finish. Every time. Eyes on the gauge.</div></div>
<<set $game_state.current_canvas = "salvage_session_4">>
<<set $game_state.current_node = "salvage_session_4.base">>
<<script>>setup.markCanvasTriggered("salvage_session_4");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_4.base") === -1) { vn.push("salvage_session_4.base"); }<</script>>
<<nobr>>
<<link "Get on him." "Canvas_salvage_session_4_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_4_fuck.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She climbs on and takes him herself — sets the pace, works him at a flat steady grind, nothing performed about it. "Same as always," he says to the ceiling. "Wake me if I'm needed." Kess doesn't look up from the readout.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Keep it steady. I don't need him loud, I need him finishing. Watch the needle when he goes.</div></div>
<<set $game_state.current_canvas = "salvage_session_4">>
<<set $game_state.current_node = "salvage_session_4.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_4.n2") === -1) { vn.push("salvage_session_4.n2"); }<</script>>
<<nobr>>
<<link "Reach for the take." "Canvas_salvage_session_4_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_4_finish.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He empties into her and she reaches — down inside for the old pull, the thing that used to crack a man open at the spine and walk his secrets out. Nothing catches. Cold socket, dead as before.</p><p>But the gauge on Kess's readout twitches. Once. A single flick of the needle, then flat.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> There. You see that? Dead through the finish — no charge, no pull — but the needle moved. First time it's moved at all.</div></div>
<<set $game_state.current_canvas = "salvage_session_4">>
<<set $game_state.current_node = "salvage_session_4.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_4.n3") === -1) { vn.push("salvage_session_4.n3"); }<</script>>
<<nobr>>
<<link "Let him go." "Canvas_salvage_session_4_Node_read">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The regular climbs off, dresses without a word, lets himself out the way he came. Kess is already crouched at the seam with the probe, chasing that one twitch back down the line, jaw set.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Still dead. Don't mistake me — you pulled nothing, and you won't for a while yet. But that flick? That's the driver taking a breath. It was flat as a stone last time. Now it's not.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> We're getting somewhere. Slow, and it costs you every visit — but somewhere. Come back when you've got coin for the next.</div></div>
<<set $game_state.current_canvas = "salvage_session_4">>
<<set $game_state.current_node = "salvage_session_4.read">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_4.read") === -1) { vn.push("salvage_session_4.read"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", -8.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_session", "add", 1.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 0.0, true, null);setup.showEffectNotification();<</script>>
[[Off the cradle.->Location_kess_berth]]<img src="./videos/scenes/salvage_session_3.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess has another body waiting — a welder off the night shift, big through the shoulders, paid and told nothing. He stands there rolling his neck, impatient the way big men get when they're being kept from the work they came to do.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Core's still cold. You'll pull nothing off him — same as last time. But I've got a part to swap while it's turning, so keep it turning. Turn it over, use him however you like.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> And reach for the take at the finish. Every time. I want to watch it fail while I've got my hands in the seam.</div></div>
<<set $game_state.current_canvas = "salvage_session_3">>
<<set $game_state.current_node = "salvage_session_3.base">>
<<script>>setup.markCanvasTriggered("salvage_session_3");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_3.base") === -1) { vn.push("salvage_session_3.base"); }<</script>>
<<nobr>>
<<link "Get to it." "Canvas_salvage_session_3_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_3_fuck.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>No easing about the welder. He bends her over the bench, gets a hand flat on her back and works into her hard and fast, all shoulder. "Quick job, then," he grunts, not asking. Kess ignores them, elbow-deep in the housing, prying a seized part loose.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Keep it turning. I've got the driver half out — don't stop till the finish.</div></div>
<<set $game_state.current_canvas = "salvage_session_3">>
<<set $game_state.current_node = "salvage_session_3.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_3.n2") === -1) { vn.push("salvage_session_3.n2"); }<</script>>
<<nobr>>
<<link "Reach for the take." "Canvas_salvage_session_3_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_3_finish.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He finishes deep and holds her there, spent. She does what Kess said — reaches down inside for the old pull, the thing that used to break a man at the spine. Nothing to reach with. The socket's still cold, still empty.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Dead. Clean through the finish, no charge — and there it is, the part in my hand, seized solid. That's your fault. That's the whole fault.</div></div>
<<set $game_state.current_canvas = "salvage_session_3">>
<<set $game_state.current_node = "salvage_session_3.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_3.n3") === -1) { vn.push("salvage_session_3.n3"); }<</script>>
<<nobr>>
<<link "Let him go." "Canvas_salvage_session_3_Node_read">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The welder pulls his coveralls up and lets himself out, paid twice and none the wiser. Kess turns the seized part over under the lamp, thumbing the pitting, almost pleased — the flat pleasure of a man who's finally cornered the thing he was chasing.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> It's the soft driver — the coupling that feeds the drain. It's not broken, it's seized. Been binding for years and you never felt it because you had power to spare. Now you don't, so it reads dead.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Good news is I know what it is now. Bad news is it comes free slow, and it costs you every visit. Come back when you've got coin for the next.</div></div>
<<set $game_state.current_canvas = "salvage_session_3">>
<<set $game_state.current_node = "salvage_session_3.read">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_3.read") === -1) { vn.push("salvage_session_3.read"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", -8.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_session", "add", 1.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 0.0, true, null);setup.showEffectNotification();<</script>>
[[Off the cradle.->Location_kess_berth]]<img src="./videos/scenes/salvage_session_2.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess has a body waiting when Wren comes in — a rigger off the night boats, paid, told nothing. Boots already off, easy about the wait.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Core's cold. You'll pull nothing off him — I know that. I need it turning over so I can find what's seized. So turn it over. Use him however you like.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> And reach for the take at the finish. Every time. I want to watch it fail — I need to see where the charge dies.</div></div>
<<set $game_state.current_canvas = "salvage_session_2">>
<<set $game_state.current_node = "salvage_session_2.base">>
<<script>>setup.markCanvasTriggered("salvage_session_2");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_2.base") === -1) { vn.push("salvage_session_2.base"); }<</script>>
<<nobr>>
<<link "Get to it." "Canvas_salvage_session_2_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_2_fuck.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>The rigger lays her back on the cradle, gets her legs up, works into her at a slow flat pace. "Strangest coin I've earned," he says. "Not the worst."</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Quiet. She's not here for you. Pressure's building at the base — I can see it load. That's the driver waking. Keep steady.</div></div>
<<set $game_state.current_canvas = "salvage_session_2">>
<<set $game_state.current_node = "salvage_session_2.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_2.n2") === -1) { vn.push("salvage_session_2.n2"); }<</script>>
<<nobr>>
<<link "Reach for the take." "Canvas_salvage_session_2_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_2_finish.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He empties into her. She reaches down inside herself for the old pull — the thing that used to open a man at the base of the spine. Nothing answers. A cold socket where the weapon sat.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Dead. Right through the finish — no charge, no pull, nothing. The socket's there, the driver just won't turn it over. Good. Clean data.</div></div>
<<set $game_state.current_canvas = "salvage_session_2">>
<<set $game_state.current_node = "salvage_session_2.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_2.n3") === -1) { vn.push("salvage_session_2.n3"); }<</script>>
<<nobr>>
<<link "Let him go." "Canvas_salvage_session_2_Node_read">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The rigger dresses and lets himself out, paid twice and none the wiser. Kess crouches behind her with a probe, chasing the fault down the seam.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> It's not the drain. Drain's fine. It's the thing that drives it that's gone soft — and that I can bring back. Slow. Costs you every visit. Come back when you've got coin for the next.</div></div>
<<set $game_state.current_canvas = "salvage_session_2">>
<<set $game_state.current_node = "salvage_session_2.read">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_2.read") === -1) { vn.push("salvage_session_2.read"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", -8.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_session", "add", 1.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 0.0, true, null);setup.showEffectNotification();<</script>>
[[Off the cradle.->Location_kess_berth]]<img src="./videos/scenes/salvage_session_1.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess has the first one waiting when Wren comes in — a dock hand off the cargo runs, paid, told nothing. Big through the shoulders, half-stripped, easy about the coin and the quiet.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> First body. Don't expect anything off him — the take's dead, I know it's dead. But I can't read a drain that isn't turning over, and yours has been cold since they hauled you in. So turn it over. Use him however you like — rough, slow, doesn't matter to me, only to the readout.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> And reach for the take at the finish. Every time, hard as you can. I want to watch exactly how it fails — the shape of the failure tells me what's left to fix.</div></div>
<<set $game_state.current_canvas = "salvage_session_1">>
<<set $game_state.current_node = "salvage_session_1.base">>
<<script>>setup.markCanvasTriggered("salvage_session_1");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_1.base") === -1) { vn.push("salvage_session_1.base"); }<</script>>
<<nobr>>
<<link "Get to it." "Canvas_salvage_session_1_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_1_fuck.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He bends her over the lip of the cradle, one hand flat on her back, and works into her from behind at a steady loading-dock pace. "Cleanest coin I've made all season," he says to no one.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Save your breath for the work. She's a bench, not a date. Steady pace — good, I can read steady. Watch her spine, right at the base, where the seam sits. That's the only part of her I care about.</div></div>
<<set $game_state.current_canvas = "salvage_session_1">>
<<set $game_state.current_node = "salvage_session_1.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_1.n2") === -1) { vn.push("salvage_session_1.n2"); }<</script>>
<<nobr>>
<<link "Keep going." "Canvas_salvage_session_1_Node_leak">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>Somewhere in the steady of it her spine trips — not arousal, a fault. Something older loads behind her eyes with no place to put it. Smoke. Not this dry-dock's oil-smoke. The kind that doesn't wash out of a person.</p><p>A bigger hand than the dock hand's. It doesn't hunt for the seam — it goes straight to the socket at the base of her spine, like it set the socket there, like it knew the map before there was a map.</p><p>A voice says a word over her — not the kind said over a thing someone owns. The kind said over a person. And under it, off the edge of hearing, one syllable of a name. It fits her like a socket she never knew was empty.</p><p>She reaches for the rest of it — reaches the way Kess told her to reach for the take — and it goes out like a held breath let go. Gone. Just the dock hand's hands again, and the cold.</p><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> ...who.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> You said something. Don't chase it — whatever that was, it's not on my readout, so it's not the job. The name, the smoke, none of it. Reach for the take instead. Now, while he's close.</div></div>
<<set $game_state.current_canvas = "salvage_session_1">>
<<set $game_state.current_node = "salvage_session_1.leak">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_1.leak") === -1) { vn.push("salvage_session_1.leak"); }<</script>>
<<nobr>>
<<link "Reach for the take." "Canvas_salvage_session_1_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/salvage_session_1_finish.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He empties into her and she does what Kess said — reaches down inside herself for the old pull, the thing that used to open a man at the base of the spine and walk his secrets into her hands. Reaches, and closes on nothing.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Dead. Right through the finish — no charge, no pull, flat line the whole way. A cold empty socket where the weapon used to sit. Good. That's clean data, though — the first clean thing I've got off you.</div></div>
<<set $game_state.current_canvas = "salvage_session_1">>
<<set $game_state.current_node = "salvage_session_1.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_1.n3") === -1) { vn.push("salvage_session_1.n3"); }<</script>>
<<nobr>>
<<link "Let him go." "Canvas_salvage_session_1_Node_read">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The dock hand dresses, pockets the second half of his coin, and lets himself out none the wiser. Kess crouches behind her with the probe, tracing the drain-seam by feel — and stops, thumb resting on the ridge of it.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Huh. This seam was re-seated. By hand. Someone had their fingers in here before me — and they didn't have to look for the socket, they went straight to it. No field-medic did this. A field-medic gropes around for it. Whoever closed you up already knew exactly where it lived.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Anyway. It's not the drain — drain's sound. It's the thing that drives the drain that's gone soft, and that I can bring back. Slow. Costs you every visit. Come back when you've got coin for the next one.</div></div>
<<set $game_state.current_canvas = "salvage_session_1">>
<<set $game_state.current_node = "salvage_session_1.read">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_session_1.read") === -1) { vn.push("salvage_session_1.read"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", -8.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_session", "add", 1.0, true, null);setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 0.0, true, null);setup.showEffectNotification();<</script>>
[[Off the cradle.->Location_kess_berth]]<p>The torch is cold on its hook and the cradle sits empty, the hull up on its blocks going nowhere. Kess keeps hours like any other working man. The black water under the walkway doesn't move. Come back when he's cutting.</p>
<<set $game_state.current_canvas = "berth_offhours">>
<<set $game_state.current_node = "berth_offhours.base">>
<<script>>setup.markCanvasTriggered("berth_offhours");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("berth_offhours.base") === -1) { vn.push("berth_offhours.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Back up the ramp.->Location_underworld_strip]]<img src="./videos/locations/kess_berth.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Kess is where he always is, up in the guts of something with a torch. He doesn't look round when Wren comes down the ramp — just tips his chin at the cradle, and at the coin-box bolted to the bench beside it.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Box first, then the cradle. Same as always. No coin, no session — go make some and come back.</div></div>
<<set $game_state.current_canvas = "salvage_repair_hub">>
<<set $game_state.current_node = "salvage_repair_hub.base">>
<<script>>setup.markCanvasTriggered("salvage_repair_hub");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_repair_hub.base") === -1) { vn.push("salvage_repair_hub.base"); }<</script>>
<<nobr>>
<<if setup.checkCostsAffordable([{"trait": "coin", "value": 10}])>>
<<link "Pay the box and get on the cradle. (10 coin)" "Location_kess_berth">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "repair_armed", "set", 1.0, true, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", -10, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<else>>
<span class="locked-choice" title="Pay the box and get on the cradle. (10 coin)">Pay the box and get on the cradle. (10 coin) (<<= setup.getCostBlockedMessage([{"trait": "coin", "value": 10}])>>)</span><br>
<</if>>
<<link "Not today." "Location_underworld_strip">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/scenes/kess_berth_intro.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The berth's colder than the strip, all cut steel and sparks, the red light of the underground shut out behind a hull. Someone's up on the work-cradle with a torch. It cuts out when Wren's boots hit the floor.</p><div class="dialog-block dialog-npc"><div class="dialog-content"><strong>Stranger:</strong> Yard's shut. Turn around.</div></div><span id="cascade-b3-beat-0"><div class="dialog-block dialog-npc"><div class="dialog-content"><strong>Stranger:</strong> ...that's Vance plate under the coat. No. I don't touch company work — company work touches back. Out.</div></div><p>He's already turning back to the hull, done with it.</p><span id="cascade-b3-beat-1"><<linkreplace "Name the fault.">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> I can't operate. Something in the core.</div></div><p>The word core turns the man's head. He comes down off the cradle, a gauge already in his hand, and holds it to the base of Wren's spine.</p><span id="cascade-b3-beat-2"><<linkreplace "Let him read it.">><div class="dialog-block dialog-npc"><div class="dialog-content"><strong>Stranger:</strong> ...that's not a Vance core. That's not anybody's core I've got a book for. Bespoke. Hand-built — somebody made you one at a time.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> I'm Kess. Sit down.</div></div><span id="cascade-b3-beat-3"><<linkreplace "Hear the terms.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Here's how it runs. I don't fix a thing like you in one sitting. I open you, work one thing, make it hold, close you up — then you come back and we go again. Session at a time, however many it takes.</div></div><span id="cascade-b3-beat-4"><<linkreplace "Ask the price.">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> What's it cost.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> Coin. Not company scrip — I can't spend Vance money without Vance noticing, and neither can you. Ten a session, and you're on the cradle.</div></div><span id="cascade-b3-beat-5"><<linkreplace "Hear the catch.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/kess.jpg" alt="Kess" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Kess:</strong> And it's up front — every session, before I open you. You're broke, I can read that off you too, so you'll be earning it up the strip between visits — the House, the Pit, whoever's paying. No coin, no session. That part's yours.</div></div>[[So that's the deal.->Location_kess_berth]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "salvage_kess_intro">>
<<set $game_state.current_node = "salvage_kess_intro.base">>
<<script>>setup.markCanvasTriggered("salvage_kess_intro");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_kess_intro.base") === -1) { vn.push("salvage_kess_intro.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "kess_met", "set");<</script>><img src="./videos/scenes/salvage_waterfront_dawn.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The phone in her hip buzzes before the light's full up. Mercer's summons — the pattern she answers on reflex, before she's decided anything. She gets her feet under her to go in and report. The bad leg folds and puts her down on one knee.</p><span id="cascade-b2-beat-0"><p>The other road, then. Renner's depot, cheap hands, the cover she can still wear. She reaches back for the drain to check it's seated, and it answers cold — a dead click where the charge should catch. Nothing takes.</p><span id="cascade-b2-beat-1"><<linkreplace "Take stock.">><p>Both roads want a body that works. Hers doesn't. The leg holds if she never asks it to turn; the drain seats but won't fire; the core sits wrong behind her sternum, a low fault she can feel the edge of and can't reach.</p><span id="cascade-b2-beat-2"><<linkreplace "Consider the company.">><p>The company fixes assets. She knows exactly what fixing is: the techs open her up on the bench, find the fault, and take her back to spec.</p><span id="cascade-b2-beat-3"><<linkreplace "Count what that costs.">><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> Back to spec is back to nothing. The four words go. The name that fit a lock she didn't know she had — filed, overwritten, gone.</div></div><p>And she finds she does not want that. She does not look at why.</p><span id="cascade-b2-beat-4"><<linkreplace "Let it ring.">><p>So she doesn't call it in. The summons rings out unanswered — the first order in her life she's let go dark. No one's watching. No one will know until it's already done.</p><span id="cascade-b2-beat-5"><<linkreplace "Go off the books.">><p>There's underworld talk of hardware worked off the books: a ship-breaker down past the gate, in the underground, who takes machines the company can't see and asks the wrong questions for the right coin. The berth's down there, off the strip. She goes to find it.</p>[[Go find it.->Location_the_waterfront]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "salvage_body_wont_hold">>
<<set $game_state.current_node = "salvage_body_wont_hold.base">>
<<script>>setup.markCanvasTriggered("salvage_body_wont_hold");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("salvage_body_wont_hold.base") === -1) { vn.push("salvage_body_wont_hold.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "salvage_entered", "set");<</script>><p>She comes back a little at a time, into a room that has changed. The men are gone. The near door stands open on an empty corridor. The frame she was strapped to is on its side where somebody knocked it going out fast. She is on the floor by the grate they meant to hose her over, and her left leg will not answer, and the light under her skin is out — properly out, not flickering, dark the way a cooled coal is dark.</p><p>Through the wall, two men are arguing.</p><p>One of them is Bastien. She would know that voice anywhere now, and it has not changed — flat, unhurried, reasonable, a man explaining a position he sees no reason to raise his voice about. The other voice she has never heard in her life, and it goes through her wrong, because it is furious, and the fury has something under it she has no file for. It takes her a long muddled moment to find the word for the thing under it, and the word she finds is grief, and that makes no sense at all, because nobody has ever once been angry on her account, let alone torn up about it.</p><p>She is too far gone to hold any of it. The words come apart before they reach her. Four get through the whole of it and no more. One is Bastien's, and it is a word she knows the shape of because it is a word men use about her: an ownership word, said the way you'd say the price of a thing. And three come across in the other voice, and one of those three is a name — she is nearly certain it is a name — and it goes into her somewhere below the amputation and fits, the way a key fits the one lock in a house that was left, a lock she did not know she had, and she cannot for her life say what the name was or why a stranger saying it should make the empty places in her ache like frostbite coming warm. And the last word, in that same broken voice, is one that has no business anywhere near a room like this, a word you say over a grave and not over a thing you are haggling for. She reaches for all four of them and they slide out of her hands.</p><p>The arguing stops. Not trails off — stops, the way it stops when one man has said the thing there is no answer to.</p><p>Then the door — the far one, off its hinges already — and he comes in, and she cannot make him hold still. He is a shape against the bad light and a smell of old smoke, and he crosses the room fast and goes down on one knee beside her, and she flinches, because everyone who has knelt beside her in this room has done it to use her. He doesn't. He turns her, careful of the dead leg, the way you'd turn something you already knew the weight of, and he finds the seam at the base of her spine without looking for it — without searching, his hand going straight to it in the dark like a hand going to its own pocket — and he puts the weight back.</p><p>The drain seats home. She feels it take, feels the terrible backed-up pressure behind her sternum find, at last, the door it has been beating on, and begin — slowly, agonisingly — to bleed off down a channel that is hers again. It is not relief. It is too late for relief. But it is the difference between a thing that is failing and a thing that is going to fail, and her leg, somewhere very far away, twitches once and remembers most of itself.</p><p>He is already up. Whatever he came to do, it was not to talk to her; or he cannot; or there is no time. He puts two fingers against the side of her face for exactly as long as it takes to be sure she has come back, and even that is not a stranger's touch, and then he is a shape in the doorway and then he is gone, and she is alone in a broken room with her weapon back in her spine and no idea on God's earth why any of it just happened.</p><p>She wakes on the waterfront at dawn. Sore, and not whole — the fault sits in her core where it stopped and does not move, and she already knows, the way you know a scar before you've seen it, that it is not going to move again. But the drain is in its socket. Her leg carries her. The city is exactly where she left it, indifferent, the gulls going and the tide out, Mercer's work waiting and Renner's depot waiting and Sunday coming around again the way Sunday does.</p><p>And four words she cannot hold, and cannot stop reaching for, and one of them a name that fit a lock she didn't know was hers.</p>
<<set $game_state.current_canvas = "captivity_cain">>
<<set $game_state.current_node = "captivity_cain.base">>
<<script>>setup.markCanvasTriggered("captivity_cain");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("captivity_cain.base") === -1) { vn.push("captivity_cain.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 1.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "emitter_broken", "set", 1.0, false, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "captivity_done", "set");<</script>>
[[Get up off the dock.->Location_the_waterfront]]<p>It doesn't happen the way she has been bracing for. There is no tearing, nothing gives, no part of her comes off in anyone's hand. What happens is quieter and much worse: the charge fills the last of the room it has, and then it keeps coming, and there is simply nowhere left for it to be.</p><p>The man in her feels it before she does — feels the whole housing of her go rigid under him, feels the hum climb past anything a body makes — and he is already scrambling back off the frame when the light goes. Not out. Up. Every seam of her, every port and plate and the long empty line of the socket Bastien stripped, all of it comes up white at once through the skin, so that for a moment she is lit from the inside like a room with the lamp knocked over, and the men are on their feet and against the walls and one of them is saying oh Christ oh Christ and none of it reaches her.</p><p>Because inside, where she has spent her whole life being quiet, it has finally gone loud. Every door she was built with is open at once. Everything the company filed away where she couldn't reach it is standing in the light — not memories, she has no memories, only the shape of the places they were cut out of, a hundred hollows all shrieking the same note. She has been a thing that feels nothing but the sex it's used for. She feels this. She feels all of it, for one white unsurvivable second, the entire amputated size of herself, and there is no word in her for what it is except that it is hers and they took it.</p><p>Then the safeties she doesn't own throw themselves, the way a station drops its own breakers before the fire, and it all goes black from the top down. The light under her skin dies. The hum falls out of the air. Her held-open eyes lose the ceiling.</p><p>The last thing that reaches her, from very far away, is the sound of the door — not the men's door, the far one, the one that has never once opened while she was awake — coming off its hinges.</p>
<<set $game_state.current_canvas = "captivity_break">>
<<set $game_state.current_node = "captivity_break.base">>
<<script>>setup.markCanvasTriggered("captivity_break");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("captivity_break.base") === -1) { vn.push("captivity_break.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "captivity_broken", "set");<</script>>
[[Go dark.->Location_captive_room]]<video src="./videos/sex/cell_he_does_not_stop.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>There's a line of them and it hasn't thinned. Someone's got her by the hips on the concrete and is working into her cunt at a flat, bored pace, and the man before him left her open and slick enough that it goes easy. That isn't the part they've stopped watching for. Under her sternum the plate has started to stutter — light coming up between her ribs in pulses, off-time with the fucking, a dull red that wasn't there when they started and won't settle back down. Every load one of them put in her ticked it higher. It has nowhere to go. She's lit from the inside like something left switched on.</p><p>The socket at the base of her spine sits empty — a dry seam in the metal where the drain used to seat. That was the part that took. Stripped out, there's only the filling: cock after cock stacking charge behind the plate with no line out, and the read-out on her chest climbs while the man fucking her doesn't notice, chasing his own thing, watching her tits move with it. The glow throbs up out of her skin and nobody backs off. They just wait their turn and fill her hotter.</p><p>'The fuck is she doing, she's lighting up.'</p><p>'Who cares, she's still tight — go, go, you're up.'</p>
<<set $game_state.current_canvas = "cell_use_he_does_not_stop">>
<<set $game_state.current_node = "cell_use_he_does_not_stop.base">>
<<script>>setup.markCanvasTriggered("cell_use_he_does_not_stop");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_he_does_not_stop.base") === -1) { vn.push("cell_use_he_does_not_stop.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_he_does_not_stop_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_he_does_not_stop_a_crew_man_balks.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>The one in her cunt pulls out mid-stroke. Not finished — he just steps back off her, cock still hard and shining, staring at the light going through her ribs like it's finally reached him what he's been putting himself into. She keeps twitching where he left her, hips still tipped up because nobody told them to come down. The glow pulses again, brighter, and something under the plate makes a sound — a thin electric whine climbing pitch — and it's the machine, not her, because her face hasn't changed at all. Her face stopped changing a while back.</p><p>He backs another step and won't go near her again, cock going soft in his fist, looking around at the others like one of them's going to explain it. None of them do. The whine keeps rising out of her chest and the red bar under her sternum flickers, gutters, catches — a light with a short in it — and still she lies open on the floor the exact way the last man left her, ass up, thighs slack, waiting to be used by whoever's next.</p><p>'Boss. Boss, that's — that's not right.'</p><p>'Whatever the fuck she is, that ain't it working. That's it breaking.'</p><p>'She's glowing, man, look at her chest, I'm not putting my dick back in that.'</p>
<<set $game_state.current_canvas = "cell_use_he_does_not_stop">>
<<set $game_state.current_node = "cell_use_he_does_not_stop.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_he_does_not_stop.n2") === -1) { vn.push("cell_use_he_does_not_stop.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_he_does_not_stop_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>Bastien crosses the floor unhurried and crouches at her chest to look — not at her face, at the read-out, at the number climbing behind the flicker. He watches it the way he watches everything about her: like a figure going into a ledger. He lays two fingers flat on the plate, feels the heat coming off it, the whine buzzing up through the metal into his hand, and he doesn't pull them away. He turns his head and looks at the man who stepped back, then at the rest of them standing there with their cocks out, gone uncertain.</p><p>Then he takes his fingers off the plate, wipes the heat off on her tit, and stands, and tells them to get back in her. He's not curious out loud. He's curious in the eyes — but his mouth is only cruel, and the two don't argue with each other in him.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> 'It's not breaking. It's filling. There's a difference and I've been watching it fill for a while now.'</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> 'You fuck her, it charges her — and somebody pulled the one part off her that let her spend it. So now it just goes in. Stacks up. Nowhere out. That's what the light is.'</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> 'Which means if she comes apart, I finally get to see what they built her out of. And there's exactly one way to reach that number. So finish. All of you. Back in her.'</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> 'She doesn't get to keep it, and she doesn't get to hold anything back either. Fill the filthy machine till it can't take another drop.'</div></div><p>'You heard him. Turn her over.'</p>
<<set $game_state.current_canvas = "cell_use_he_does_not_stop">>
<<set $game_state.current_node = "cell_use_he_does_not_stop.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_he_does_not_stop.n3") === -1) { vn.push("cell_use_he_does_not_stop.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_he_does_not_stop_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_he_does_not_stop_used_past_failure.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>They turn her over. Someone hooks her by the ankle and folds her onto her front, and she goes where she's put — arms loose under her, cheek flat to the concrete — and then there's weight across her back and someone's in her ass now, driving hard the second the boss said go. Her legs stay where they set them, knees shoved apart, ass tipped up, because she's stopped bracing, stopped everything that isn't the plate. And it's her ass — the socket right there above the crack, dead and empty, the drain that used to take pulled out of it. Nothing takes now. Every thrust into her only feeds the thing that fills. The red bar between her ribs has stopped pulsing and gone solid, a level rising, and each grunt and each load somebody empties into her walks it up toward the top.</p><p>One of them reaches under her and works her clit rough while the man behind fucks her, because the boss said the fast way is to make her come — and when it wrenches through her the read-out jumps three marks at once and something under the plate flares white. It isn't pleasure on her at that point. It's overflow with no drain, and it burns; the charge climbing through her with nowhere to be, cooking her from the plate out. She makes a sound she hasn't made — thin, mechanical, wrong — and they laugh and hold her down and drag it out of her again.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> 'Make her come. That's what moves it. Anyone can just fuck her — I want her spiking.'</div></div><p>'There it goes — you see it jump? Do it again, rub her out.'</p><p>'Hold her, hold her, she's going, she's fucking going—'</p><p>'Look at that light, it don't stop where the last one did.'</p>
<<set $game_state.current_canvas = "cell_use_he_does_not_stop">>
<<set $game_state.current_node = "cell_use_he_does_not_stop.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_he_does_not_stop.n4") === -1) { vn.push("cell_use_he_does_not_stop.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_he_does_not_stop_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/scenes/cell_he_does_not_stop_the_read_out_the_break.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>The read-out reaches the top of its climb and stops — not because they've stopped, but because there is no higher left for it to go. The bar under her sternum is full end to end, a hard flat line of light burning through the skin between her ribs, too bright now to look at straight. The whine has gone past sound into a pressure you feel in your teeth. Everywhere else she's dark: face slack, eyes open on nothing, body still moving only because the man behind her is still moving it, hips still working her onto him out of habit and weight. Every small resistance the machine ever had — gone at once, given up together.</p><p>Bastien stands over the top of her head watching the plate, not her, the way he has the whole time. Charged to the top. No socket. No out. Something in her has to give with all that light and nowhere for it, and under the flat glare of the full read-out — while a man is still emptying into her ass and doesn't even notice — it does.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> 'There. Top of the gauge. Now we find out what they were hiding in you.'</div></div><p>'Boss — boss, it's not moving anymore, it's just—'</p>
<<set $game_state.current_canvas = "cell_use_he_does_not_stop">>
<<set $game_state.current_node = "cell_use_he_does_not_stop.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_he_does_not_stop.readout") === -1) { vn.push("cell_use_he_does_not_stop.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -14.0, true, null);setup.showEffectNotification();<</script>>
[[Take the last of it.->Location_captive_room]]<video src="./videos/sex/cell_stutter.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>The mattress lost its sheet a long stretch ago. She's face-down on the bare ticking, hips hauled up, and the man behind her has both hands clamped on her waist, fucking into her cunt at a flat bored pace — the pace of a thing he's used long enough to stop looking at. He isn't working for it. He's just emptying himself into a hole that's been left open for him.</p><p>Under her sternum the plate that used to burn clean white sits dim now — a dull ember guttering through her skin with every shove, the charge stacked so high behind it that it's stopped glowing right. Each time his cock bottoms out, the read-out over the plate strobes and climbs another notch. Nobody in the room is bothering to read it yet. They will.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Look at it take that. Filthy machine doesn't even need telling anymore — just lies there and lets itself get bred.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Barely lit. Whole thing's running on fumes. Good.</div></div>
<<set $game_state.current_canvas = "cell_use_stutter">>
<<set $game_state.current_node = "cell_use_stutter.base">>
<<script>>setup.markCanvasTriggered("cell_use_stutter");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_stutter.base") === -1) { vn.push("cell_use_stutter.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_stutter_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_stutter_her_leg_dies.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>It goes between one thrust and the next. Her left leg — braced, taking her weight on the knee — simply stops. The muscle lets go all at once, the joint unlocks, and the leg slides out flat across the ticking, dead weight, slack from hip to heel. No twitch. No shudder back to life. It just quits.</p><p>Her hip drops crooked with nothing under it holding the line, and the man's next stroke shoves her sideways across the mattress because half of what used to brace her isn't answering anymore. Somewhere under the skin a thing that was supposed to keep working has gone dark and stayed dark, and the rest of her body keeps getting fucked around the failure like it never happened.</p>
<<set $game_state.current_canvas = "cell_use_stutter">>
<<set $game_state.current_node = "cell_use_stutter.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_stutter.n2") === -1) { vn.push("cell_use_stutter.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_stutter_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_stutter_he_keeps_going.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He doesn't stop. He barely slows — just reaches down, catches the dead leg under the knee, and hooks it up over his forearm to get the crooked hip back where he wants it. Then he fucks her exactly as before, harder if anything, the slack limb swinging loose off his arm with every drive of his cock like a thing that's already been thrown out.</p><p>The rest of the room watches a machine's leg give out and does the only arithmetic that matters to them: it's still a hole, and the hole still works. Nobody moves to stop it. Nobody would. They lean in closer instead, to see.</p><p>"Its fuckin' leg just died — keep going, keep going, don't you dare stop!"</p><p>"Leg's dead, hole's not. Who cares."</p><p>"Ride it till the next bit falls off."</p>
<<set $game_state.current_canvas = "cell_use_stutter">>
<<set $game_state.current_node = "cell_use_stutter.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_stutter.n3") === -1) { vn.push("cell_use_stutter.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_stutter_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/scenes/cell_stutter_bastien_leans_in.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Bastien comes off the wall and crouches at the edge of the mattress, close, elbows on his knees, watching the dead leg swing where it hangs off the man's arm. Not her face — her face has never been the thing. He watches the failure. He reaches out and pushes two fingers deep into the slack meat of her thigh, testing, feeling for a tone that isn't there anymore, and something moves behind his eyes that has nothing to do with mercy.</p><p>He's interested. He's always most interested when a part of her stops. He keeps his fingers buried in the dead muscle a moment longer than he needs to, thumb pressing, reading her by hand while the man's cock keeps working her hole two inches away.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There it goes. Left side's dropping out — right on the number I said it would.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Don't you fucking stop. I want to see what quits next.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Look at that. Bought a machine, and it's coming apart while it gets used. Beautiful.</div></div>
<<set $game_state.current_canvas = "cell_use_stutter">>
<<set $game_state.current_node = "cell_use_stutter.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_stutter.n4") === -1) { vn.push("cell_use_stutter.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_stutter_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/scenes/cell_stutter_the_read_out.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>On the sternum plate the read-out has climbed again — one more increment, logged the second the man's pace picked back up. Every stroke into her stacks a little more charge behind a plate that has nowhere left to send it. The socket at the base of her spine, where the drain used to bleed it off — where anal used to take something out of a man instead of pour it into her — sits empty and cold, a dead port in the small of her back that nothing plugs anymore.</p><p>The number does not fall. It has never once fallen. Bastien's eyes go straight to it and stay there — not on the swinging leg, not on the crew, not on her — on the count. He takes out the little book and writes the new figure under all the others, unhurried, while behind the number the plate flickers like it's finally getting tired.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Higher. Every time it fails, the number still climbs. That's the part they never believe.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Keep filling it. It's got nowhere to put any of it.</div></div>
<<set $game_state.current_canvas = "cell_use_stutter">>
<<set $game_state.current_node = "cell_use_stutter.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_stutter.readout") === -1) { vn.push("cell_use_stutter.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -14.0, true, null);setup.showEffectNotification();<</script>>
[[Stay where she's dropped.->Location_captive_room]]<video src="./videos/sex/cell_overflow_legs_held_open.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>The crew are already in the room when it starts. No unwrapping, no chair pulled up slow — that part's over, she's been stripped since before they came in. Two of them take a leg each and fold her open on the mattress, a knee pinned to the concrete on either side, a forearm braced across each thigh, a hand clamped on each ankle. She's spread and she's held and nothing's inside her yet.</p><p>That's the point of holding her first. What they're about to do works better if she can't close on it, so they get her open and locked before a single cock touches her. The plate under her sternum reads high enough today to be worth the trouble, and every man in the room knows what a high reading means: she's easy to push over now, and pushing her over is the whole job.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Hold her open. I want the reading clean when she goes.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Look at it — a machine that fills up like a cheap whore and can't even spill what's in it. Bought and paid for, and it doesn't even work.</div></div><p>"Get the legs — spread the filthy thing wide."</p><p>"It can't even close, look at it, it's just held there."</p><p>"Stay still, hole. You're gonna want to be real still for this."</p>
<<set $game_state.current_canvas = "cell_use_overflow">>
<<set $game_state.current_node = "cell_use_overflow.base">>
<<script>>setup.markCanvasTriggered("cell_use_overflow");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_overflow.base") === -1) { vn.push("cell_use_overflow.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_overflow_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_overflow.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>A man kneels into the gap they've made her hold and pushes into her cunt, and he doesn't fuck her so much as work her — a thumb set on her clit, a rhythm picked cold, not to get himself off but to get her off, because that's the order he was given. They're not here to come in her. They're here to make her come. A forced orgasm spikes the read-out harder than anything else they can do to a body like hers, and every man in the room has learned that now, and they're grinning at it.</p><p>She holds against it because there is nothing else left to do with her hands full and her legs locked, and it doesn't matter. The machine comes when it's made to. He works her over the edge with the flat patience of a man doing paid work, and behind her sternum something starts to climb toward the wall it always finds now.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Make her spill. Doesn't matter what the thing wants — a machine comes when it's made to, and the number jumps every time it does.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There. Force it out of her. That reading's worth twice what just fucking her is — so make the filthy thing come, and mind you get it all the way.</div></div><p>"Come on, machine, give it up."</p><p>"Look at it fight — it doesn't want to, that's the best part."</p><p>"Doesn't matter what it wants, does it. Push it over."</p>
<<set $game_state.current_canvas = "cell_use_overflow">>
<<set $game_state.current_node = "cell_use_overflow.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_overflow.n2") === -1) { vn.push("cell_use_overflow.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_overflow_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_overflow_it_hurts_the_overflow.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>And she comes, and it's wrong. The charge goes off behind her sternum and slams into a wall that a body built right doesn't have — the heat that used to run out through the drain at the base of her spine has nowhere to run, so it turns and runs back up into her chest and stacks on top of the last load, and the coming stops being pleasure halfway through and becomes a fault firing down a line. She screams. Not the noise a whore makes on cue — a real one, torn out of her, the sound of a thing overloading with the vents shut.</p><p>The crew think it's the funniest thing they've seen all week. She screamed coming, and it clearly hurt her, and that just makes them want another one out of her. Someone's laughing hard enough to lean on the wall.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> That's the overflow. No channel — it's got nowhere to put itself, so it puts it in her. Scream all you want, filthy thing. The number doesn't care, and I paid for the number.</div></div><p>"Ha — hear that? It hurts it! It hurts the machine to come!"</p><p>"It's crying, look — do it again, make it scream again."</p><p>"Whores don't scream when they come, sweetheart. You're broke."</p><p>"Doesn't even work right and we're still using it."</p>
<<set $game_state.current_canvas = "cell_use_overflow">>
<<set $game_state.current_node = "cell_use_overflow.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_overflow.n3") === -1) { vn.push("cell_use_overflow.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_overflow_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_overflow_they_keep_going.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>It changes nothing. They take turns on her. One pulls out and the next kneels straight in, her cunt then her ass by turns, and every load that goes into her adds a step to the thing stacked behind her sternum and takes nothing back out. The socket at the base of her spine sits empty through all of it — the one thing that ever drew any of this back out of her, stripped clean and gone — so her ass isn't a drain any more, it's just another hole that fills her. It only ever adds now. Nothing leaves.</p><p>Bastien has dragged his chair up close. He is not watching her face, or her cunt, or the man buried in her. He is watching the plate under her sternum, where the light has stopped sitting steady and started to climb and hold, and he is writing the number down in a book propped on his knee, checking it against her chest between loads the way a man reads a gauge that's running hot.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Keep her going. It stacks — nothing comes back out of a machine with its drain pulled, so pour it all in.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Filthy overfilled thing. You'll take every one of them and hold every drop, and the reading goes up each time. That's all you're for now.</div></div><p>"Next — get in the hole, it can't spill."</p><p>"Fill it right up, it's got nowhere to put it."</p><p>"Passed round like it's nothing, this one — and it takes it every time."</p>
<<set $game_state.current_canvas = "cell_use_overflow">>
<<set $game_state.current_node = "cell_use_overflow.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_overflow.n4") === -1) { vn.push("cell_use_overflow.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_overflow_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>When they finally leave her spread and slack on the mattress, the plate under her sternum is not doing what it did before they started. The light in it has stopped being steady and gone to a pulse, higher than she has ever seen it read, and it does not come down. It sits there, lit, climbing on nothing, with no drain to bleed it off.</p><p>Bastien crouches over her and looks at it — only at it. He reads the number, writes the number, and the number is the whole reason he came. It went up. It does not go down. Out of everything that was done to her in this room, that is the one thing that changed, and it is the only thing in here that he counts as changed at all.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There's the reading. Higher than the last. You're closer now, machine — and every hole they put in you drives it up another notch and it never once falls.</div></div>
<<set $game_state.current_canvas = "cell_use_overflow">>
<<set $game_state.current_node = "cell_use_overflow.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_overflow.readout") === -1) { vn.push("cell_use_overflow.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -14.0, true, null);setup.showEffectNotification();<</script>>
[[Come down off it.->Location_captive_room]]<video src="./videos/sex/cell_frame_strapped_into_the_rig.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>The frame is bolted to the floor before she's brought to it — a waist-high steel jig with a padded bar and cuffs at the four corners, built to hold something down while it gets worked on. They don't wrestle her into it. They set her. Fold her over the bar, feed her wrists into the cuffs, wrench the strap across the small of her back and crank it until she's fixed face-down at the exact angle they want, ass up, knees spread by the frame's own geometry, the empty socket at the base of her spine sitting open to the room.</p><p>Not held down. Positioned. Locked to spec, like a part dropped into a machine that's about to run — no hands needed once the straps bite, because a fixture doesn't get to move. Someone tests it with a shove and she doesn't shift an inch. The frame takes it for her.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Bolt it down. I don't want the machine wandering off while it does its job.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Ass up, hole open, spine to the ceiling. That's the only shape it's good for now — so lock it there and leave it.</div></div>
<<set $game_state.current_canvas = "cell_use_frame">>
<<set $game_state.current_node = "cell_use_frame.base">>
<<script>>setup.markCanvasTriggered("cell_use_frame");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_frame.base") === -1) { vn.push("cell_use_frame.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_frame_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_frame_positioned_tuned.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>A crank on the side of the frame lifts the bar an inch; a boot hooks her knee and drags it wider until the cunt and the empty ass sit at the height a standing man wants. A fist takes her hair and tips her head off the frame to clear the throat, lets it drop, tips it again — checking the line. They talk over her the whole time, the way men talk over a bench they're setting up. Not to her. About her.</p><p>Someone works two fingers into her ass and spreads, eyes the angle, wipes the hand off on her thigh. Someone else thumbs her cunt open, lets it close, satisfied. Nobody asks if she's ready because ready isn't a setting the rig has. Tuned. Zeroed. Set to spec and left waiting at the height of the line.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Zero the readout before anyone touches it. I want a clean count off this run — every load logged, none of it wasted.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Height's good. Now it's just a hole at the right elevation. Start the line.</div></div><p>Bring her up an inch — I'm not squatting for a machine.</p><p>There. That's the height. Lock it.</p><p>Spread's good. Both holes on the same level, take your pick.</p><p>Wipe your hand on it, it's not gonna complain.</p>
<<set $game_state.current_canvas = "cell_use_frame">>
<<set $game_state.current_node = "cell_use_frame.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_frame.n2") === -1) { vn.push("cell_use_frame.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_frame_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_frame.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Then they run her. One after another down the line — cunt, then ass, then whichever's nearest, no order to it, the frame absorbing every shove so she never moves off her mark. Each one fucks until he's finished and then stays, buried to the root, leaning his weight down on the small of her back to catch his breath, using her like a place to rest before he pulls out and the next steps up into the gap. Nobody talks to her. She's the bench they're all taking a turn on.</p><p>And every load that goes in lands with nowhere to go. The socket at the base of her spine sits empty where the drain used to bolt in — the ass that used to TAKE something out of a man now just filling, over and over, the drain inverted. Each one that gets held in her feeds the plate under her sternum. The charge stacks behind it and climbs, and the read-out ticks and does not drop.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> That's it — hold it in when you're done. Every one you leave sitting in there winds the number tighter than the fuck did.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Don't rush the line. It's not going anywhere. It's a fitting.</div></div><p>Furniture with a hole. Best kind.</p><p>Move — my turn. And don't pull out slow, leave it in there for the meter.</p><p>Still tight after all that? Machine earns its keep.</p>
<<set $game_state.current_canvas = "cell_use_frame">>
<<set $game_state.current_node = "cell_use_frame.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_frame.n3") === -1) { vn.push("cell_use_frame.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_frame_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_frame_held_inside_marks.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>One of them doesn't pull out at all. He drives in to the root and stops, grinds her down flat onto the bar and just holds there — a full beat, two, hips flush to her ass — while his hands go to work on everything else. A fistful of hair cranks her head back off the frame. The other hand wraps her throat from behind and squeezes until the read-out jumps, then lets go and leaves the print of every finger behind on her skin.</p><p>He slaps the ass he's still buried in, hard, twice, watches the handprint bloom red and stay. Digs his thumbs into her hips until they bruise, marking his grip where the next man can see it. By the time he finally pulls out she's covered — handprints across both cheeks, choke-marks coming up at her throat, drool dried on her chin, thumb-bruises on her hips. A machine handled rough and left marked all over, so anyone in the room can read exactly where it's been.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Choke it while you're seated in. The readout spikes when it can't breathe — fastest way to run the number up is to make the thing come while it's strangling.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Mark it up. I want to be able to see everywhere it's been, from across the room.</div></div><p>Look at that print come up. Take a picture.</p><p>Leave your grip on it — save the next guy a handhold.</p>
<<set $game_state.current_canvas = "cell_use_frame">>
<<set $game_state.current_node = "cell_use_frame.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_frame.n4") === -1) { vn.push("cell_use_frame.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_frame_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>When they're finally done and the frame's cranked back and she's left bent over it — dripping, marked, still bolted to spec — the only thing in the room still doing work is the plate under her sternum. The read-out glows in the dim, and the number on it is higher than it was. A lot higher. It climbed with every load that got held in her and it did not come back down, because there is nowhere for it to go.</p><p>Bastien crouches at the front of the frame to read it. Not her face — the number. He watches it settle, holds a light to it, writes it down.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Up again. Big jump off this run. It takes it in and it can't give any of it back — that's the whole trick of the thing.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Nowhere near topped out. Log it, reset the frame. We're not done running numbers off it.</div></div>
<<set $game_state.current_canvas = "cell_use_frame">>
<<set $game_state.current_node = "cell_use_frame.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_frame.readout") === -1) { vn.push("cell_use_frame.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -12.0, true, null);setup.showEffectNotification();<</script>>
[[Let them unstrap her.->Location_captive_room]]<video src="./videos/scenes/cell_turns_the_queue.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>By the time she's down on her knees in the middle of the concrete, the crew's already strung out along the wall in a loose line, waiting their turn like it's a thing they've done before. Bastien doesn't get in the line. He hauls the chair round so it faces the whole row of them, sits, and clicks a timer open in his palm. His cock's already out. His eyes are on the plate under her sternum, not on her.</p><p>'Thirty a man to start,' he says to the room. 'Move her down the line, don't crowd. I want to see how fast this thing climbs when you keep it fed.' The first man's already got a fist in her hair, waiting on the word. Bastien thumbs the timer. 'Go.'</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Thirty a man. Don't crowd her — I want it steady, not a pile-on. Not yet.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Look at it. Bought and paid for, and it takes a line better than a turnstile.</div></div><p>'Fuckin' finally. Been standing here with it out.'</p><p>'Save some, she's not going anywhere.'</p><p>'It's got more holes than the three of us need. Line moves.'</p>
<<set $game_state.current_canvas = "cell_use_turns">>
<<set $game_state.current_node = "cell_use_turns.base">>
<<script>>setup.markCanvasTriggered("cell_use_turns");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_turns.base") === -1) { vn.push("cell_use_turns.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_turns_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_turns_passed_down_the_line.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>The first one fucks her mouth till he's done and pulls out to finish across her cheek, and before she's coughed it clear the next man's already stepping up and turning her by the jaw. Mouth, then cunt, then mouth again — whoever's up takes whichever hole he wants and the crew calls it out while they wait. One holds her hips and fucks her through it kneeling; the next just wants her face and uses it. Nobody waits for her to be ready. Ready was never on the list.</p><p>It stacks on her and in her as it goes. Cum on her cheek, roped off her chin, a load left in her cunt that the next man just shoves back up into her with his own. By the fourth turn she's slick and dripping and the line hasn't even thinned. Bastien clicks the timer over at every swap and reads the plate, not her face, calling the number out loud like a man calling races.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Next. Don't be gentle — it doesn't get you a better number.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There it goes. Half a point a load. Keep it moving, it's finally earning its keep.</div></div><p>'Turn it round, I want the cunt.'</p><p>'Swallowed that one — good little hole.'</p><p>'Hurry up, I'm not done looking at what I'm getting.'</p>
<<set $game_state.current_canvas = "cell_use_turns">>
<<set $game_state.current_node = "cell_use_turns.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_turns.n2") === -1) { vn.push("cell_use_turns.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_turns_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_turns.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Someone gets tired of the line and bends her over a knee, spreads her ass open with both thumbs, and works into the one hole that used to do the taking. He fucks it rough and finishes deep and doesn't pull out till he's soft. And that's the joke of her nobody in the room knows: the drain lived here once, at the base of her spine, and the ass was the thing that TOOK something out of a man. The socket's stripped now. Empty. So it only fills her — the drain run backwards, a load in the ass that goes nowhere but up.</p><p>Someone else reaches under and works her clit while the first man's still buried in her, mean and deliberate, forcing her over whether she wants it or not — and when she clenches and comes the plate jumps, a hard tick, faster than any load did. Bastien sees it. 'There,' he says. 'That's the one that moves it.' He writes the number down. The charge climbs with nowhere left to go, and the stripped socket at the base of her spine does nothing at all.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> In the ass — that's where the good reading is. Trust me on that one.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There. Forced, it spikes. Make the machine come, don't just fuck it — climbs twice as fast.</div></div><p>'Rip that little hole open, watch it just take it.'</p>
<<set $game_state.current_canvas = "cell_use_turns">>
<<set $game_state.current_node = "cell_use_turns.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_turns.n3") === -1) { vn.push("cell_use_turns.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_turns_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_turns_used_and_pissed_on.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>By the time the line's thinned she's a mess on the floor, leaking out of both holes, cum drying tight on her tits and her face. One of them steps up like it's his turn again, but he doesn't get hard — he just plants his feet over her, takes his cock in his hand, and pisses on her instead. Down her chest, into her open mouth, across the plate under her sternum, hosing off what the others left like she's a thing you rinse and not a person.</p><p>The crew loses it. Someone tips her chin up by the hair so it runs across her face and she has to hold it. Nobody stops him. When he's done he shakes it off on her and steps back into what's left of the line, and she kneels there in the wet of it, dripping, used up, a thing to empty into and onto and nothing more.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Christ. Fine — it rinses off same as anything else. Doesn't move the number, though. Do that on your own time.</div></div><p>'Open up, machine — waste not.'</p><p>'Look at it just take that too.'</p><p>'Rinse and reuse, boys.'</p>
<<set $game_state.current_canvas = "cell_use_turns">>
<<set $game_state.current_node = "cell_use_turns.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_turns.n4") === -1) { vn.push("cell_use_turns.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_turns_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/scenes/cell_turns_the_read_out.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>When the last of them steps back the room goes quiet except for her breathing. Bastien crouches in front of her — the only time he gets close — and wipes the plate under her sternum clean with his thumb so he can read it. The number's higher than when they started. A lot higher. It went up with every load and it jumped with the ones they forced, and it hasn't shed a single point, because there's nothing left in her that lets it.</p><p>He looks at it a long moment. Not at her — through her, at the reading, the way you'd check a gauge creeping toward a line you drew yourself. He clicks the pen and writes it down. He caps it. He doesn't look at her face once.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Higher than the last count, and it didn't drop a point. Nothing drains out of you anymore, does it. Good.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Faster than I thought. We'll do this again before it's had time to sit.</div></div>
<<set $game_state.current_canvas = "cell_use_turns">>
<<set $game_state.current_node = "cell_use_turns.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_turns.readout") === -1) { vn.push("cell_use_turns.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -12.0, true, null);setup.showEffectNotification();<</script>>
[[Wait for the next one.->Location_captive_room]]<p>The bolt drags back and for once it isn't just him in the gap. He holds the door open with one hand and lets three of them file past his shoulder into the cell, then crosses to the chair against the wall and sits, one ankle up on his knee, the notebook already open on his thigh. The three spread out around her the way men spread out around something cornered — looking, deciding what she is before a single hand's on her.</p><p>That's where she stops being a person. Three sets of eyes, three sets of hands, and the one in the chair who only wants the number after.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Go on. She's bought and paid for — there's nothing in there you can wear out that I'm precious about.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Mind the plate on her chest, though. I want to read it when you're done with her.</div></div><p>'This the one? Christ.'</p><p>'They really do build 'em with a hole for everything.'</p>
<<set $game_state.current_canvas = "cell_use_first_three">>
<<set $game_state.current_node = "cell_use_first_three.base">>
<<script>>setup.markCanvasTriggered("cell_use_first_three");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_first_three.base") === -1) { vn.push("cell_use_first_three.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_first_three_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_first_three_manhandled.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>They don't wait for him to finish talking. A fist knots in her hair and turns her by it; another catches her wrists and wrenches them back; a third checks her over rougher than Bastien ever bothers — grabs her jaw, thumbs her mouth open, slaps her tits just to watch them move. Spit lands on her cheek and nobody wipes it off. A hand closes on her throat and squeezes, not to choke, just to feel her take it and hold there.</p><p>Bruises come up under the grips — handprints going red on her hips, on her arms, a mark on her jaw where a thumb dug in. First time it's more than one pair of hands. They pass her between them like they're testing she'll hold together.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Marks are fine. Nothing that opens the casing — she stays sealed.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Filthy machine's been handled worse than you three. Watch — she won't even flinch.</div></div><p>'Hold still, machine.'</p><p>'She don't even blink — spit on her, watch.'</p><p>'Grab-marks come up lovely on this one.'</p>
<<set $game_state.current_canvas = "cell_use_first_three">>
<<set $game_state.current_node = "cell_use_first_three.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_first_three.n2") === -1) { vn.push("cell_use_first_three.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_first_three_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_first_three.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>They put her on her knees and open her at both ends at once. One fists her hair and feeds his cock down her throat till she gags around it, holds her there while drool ropes off her chin onto her tits; the other's behind her with his cock buried to the root in her cunt, driving her back onto the first with every thrust so she can't pull off either. She's a length of holes stretched between two men and worked from both directions.</p><p>Every stroke into her charges the core. The read-out under her sternum ticks up with it — and it does not tick back down. Bastien watches from the chair, writes one line, watches again.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> She was built to be used at both ends, weren't you. Filthy fucking thing.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Charge is climbing on penetration alone — note that. Nobody's even made her come yet.</div></div><p>'Look at her take it both ends.'</p><p>'Push her — there, all the way down, that's a good hole.'</p>
<<set $game_state.current_canvas = "cell_use_first_three">>
<<set $game_state.current_node = "cell_use_first_three.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_first_three.n3") === -1) { vn.push("cell_use_first_three.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_first_three_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_first_three_dp.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>The third doesn't wait his turn. They stack her — one under her with his cock in her cunt, one still fucking her throat, and the third lines up behind and shoves into her ass. The stripped socket sits empty at the base of her spine, right above where he's splitting her open. The ass used to be the drain — the part that TOOK something out of a man. Now it only fills her, the whole thing running backwards, and every cock in her stacks the charge higher.</p><p>Then one reaches under and works her clit while she's plugged at all three, forces the orgasm up out of her whether she'd give it or not — and that's when the plate jumps. A come wrung out of her spikes it worse than any thrust. At this load it isn't pleasure landing anymore; it's charge with nowhere to go, the overflow gone hot and wrong under her sternum, and it climbs and climbs.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There it is. Every hole loaded and the number won't stop.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Make her come — that's the one. Forced spikes it hardest. Confirmed.</div></div><p>'All three holes — she's fuckin' full.'</p><p>'Feel that? Something's buzzing inside her.'</p><p>'Machine's overheating — don't stop, wring it out of her.'</p>
<<set $game_state.current_canvas = "cell_use_first_three">>
<<set $game_state.current_node = "cell_use_first_three.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_first_three.n4") === -1) { vn.push("cell_use_first_three.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_first_three_Node_n5">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_first_three_they_finish_on_in_her.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>They finish where they like. One pulls out of her throat and empties across her face — cum in her lashes, on her lips, dripping off her chin. One comes deep in her cunt and one across her back, ropes of it laid over the handprints they left there. They wipe their cocks clean on her tits and her hair when they're done with her.</p><p>She kneels there striped and dripping and does not move. Bastien counts the loads off the plate — every one of them registered, every one banked, none of it going anywhere.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Look at the state of you. Passed round and filled up like the thing you are.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Every load logged on the plate. She banks all of it — nothing drains.</div></div><p>'Open your mouth, machine — there, hold it.'</p><p>'All over her back, look at that.'</p><p>'Leave it on her. Let him see what he paid for.'</p>
<<set $game_state.current_canvas = "cell_use_first_three">>
<<set $game_state.current_node = "cell_use_first_three.n5">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_first_three.n5") === -1) { vn.push("cell_use_first_three.n5"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_first_three_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>When they've filed out he crosses the cell, crouches in front of her, and wipes a smear of cum off the plate under her sternum with his thumb to read it clean. The read-out has climbed hard — further in one session than in a week of just him alone. He doesn't look at her face. He looks at the number, writes it down, and underlines it.</p><p>The strain's up with it. The charge stacks behind the plate with nowhere to go, hotter each load, and the meter only ever runs one direction.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Three at once and forced to come — fastest climb yet. Good.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Charge has nowhere to go and it's stacking. Note the strain.</div></div>
<<set $game_state.current_canvas = "cell_use_first_three">>
<<set $game_state.current_node = "cell_use_first_three.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_first_three.readout") === -1) { vn.push("cell_use_first_three.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -12.0, true, null);setup.showEffectNotification();<</script>>
[[Stay on the floor.->Location_captive_room]]<p>The notebook comes in before he does — a cheap spiral thing he sets on the crate, squares up with the edge, uncaps a pen. Then he drags the chair round to face her and sits, not close, the way a man settles in to watch a gauge rather than fuck a woman. 'Not fucking you today,' he says, almost bored. He taps his own sternum, where hers carries the plate. 'Today I want to see the needle move. You come, that thing counts it. So we're going to find out how many I can get out of you before it complains.'</p><p>He leans in and reads the read-out under her sternum through the strip of grime, and he writes that number down before he's laid a hand on her — a baseline, neat, underlined twice. Cold about it. Curious. 'Filthy little machine,' he says, unhurried, more to the page than to her. 'Let's see what you do.'</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Not fucking you today. Today I want to see the needle move.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> You come, that thing counts it. We're going to find out how many I can get out of you before it complains.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Filthy little machine. Let's see what you do.</div></div>
<<set $game_state.current_canvas = "cell_use_charge_test">>
<<set $game_state.current_node = "cell_use_charge_test.base">>
<<script>>setup.markCanvasTriggered("cell_use_charge_test");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_charge_test.base") === -1) { vn.push("cell_use_charge_test.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_charge_test_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_charge_test.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He works two fingers into her cunt without ceremony and starts — not for her, never for her, his eyes stay up on the plate the whole time. Slow, patient, the heel of his hand grinding a circle while his thumb finds the spot and stays on it. He isn't trying to please her. He's trying to trip a switch. 'There it is,' he murmurs, feeling her clench around him, and he doesn't sound pleased so much as confirmed. 'Wet already. Built to. You can strip everything out of a thing and it still slicks up on command like a good little hole.'</p><p>When the fingers stall he puts his mouth on her too — tongue flat and mean and efficient, spit and friction, throttling her up the way you'd rev an engine — and the whole time one eye stays on the read-out under her sternum, waiting for the twitch. 'That's it. Right up to it.' He pulls back a fraction and keeps her hanging there, deliberate. 'I want the number, not you. Don't matter what you want.'</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There it is. Wet already. Built to. You can strip everything out of a thing and it still slicks up on command like a good little hole.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> That's it. Right up to it.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> I want the number, not you. Don't matter what you want.</div></div>
<<set $game_state.current_canvas = "cell_use_charge_test">>
<<set $game_state.current_node = "cell_use_charge_test.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_charge_test.n2") === -1) { vn.push("cell_use_charge_test.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_charge_test_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_charge_test_she_comes_degraded_for_it.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>It goes off without her say-so — her cunt clamps down on his fingers and she comes, hard, hips jerking up into a hand that owns her, a sound tearing out of her that isn't a choice. He doesn't ease off through it; he holds her on the edge and wrings it out, watching her body betray her from the inside. 'There she is,' he breathes. 'Look at you. Filthy little machine, creaming all over my hand, gagging for it — didn't even ask you nice.'</p><p>Under her sternum the read-out jumps — not a crawl, a jump, the forced one spiking it the way the slow ones never do — and the charge it books has nowhere to go: the socket at the base of her spine sits empty where the drain used to seat, so it just stacks, hot, behind the plate. He watches the number leap and grins and writes it down. Not her face. The number. He wipes his fingers off on her thigh and reads the new digits out loud like a score.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There she is. Look at you. Filthy little machine, creaming all over my hand, gagging for it — didn't even ask you nice.</div></div>
<<set $game_state.current_canvas = "cell_use_charge_test">>
<<set $game_state.current_node = "cell_use_charge_test.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_charge_test.n3") === -1) { vn.push("cell_use_charge_test.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_charge_test_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_charge_test_again_overstim_slaps.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He goes straight back down on her before the first has finished ringing out, and this time it's too much and that's the point — fingers hard on a clit still fizzing, no mercy in it, dragging her up the raw wall of it while she's still shaking. When she flinches away he slaps her for it, open-handed, across the cunt — a wet crack — then again across the inside of her thigh, watching the red bloom up under his palm. 'Hold still. You'll move the number or you'll wear my hand, machine. Pick one.'</p><p>The second one hurts coming out — the overflow's got nowhere to sit and it turns the pleasure to a hot ache behind her ribs — but he forces it anyway, that's the whole trick, the forced ones move it fastest. She comes with a bruise already coming up on her thigh and a handprint on her cunt, jerking against a grip that doesn't care, and the read-out climbs again. He checks the plate before he checks anything else. 'Good. Again.'</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Hold still. You'll move the number or you'll wear my hand, machine. Pick one.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Good. Again.</div></div>
<<set $game_state.current_canvas = "cell_use_charge_test">>
<<set $game_state.current_node = "cell_use_charge_test.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_charge_test.n4") === -1) { vn.push("cell_use_charge_test.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_charge_test_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_charge_test_again_read_out.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He sits back on his heels, flexes his slick hand, and writes the new figure under the last — a little column building down the page now, each line higher than the one above it. He tips the plate toward the light with two fingers under her chin, not to see her, to read her, comparing the digits against the number he set down at the start. Then he turns the page. 'Climbed,' he says, satisfied, tapping the pen. 'You went up three just now. Nice steep little curve you've got.'</p><p>It doesn't drop back. That's the thing he's writing down and she isn't saying: the read-out ticks up and it stays up, every one he forces out of her stacked behind the sternum plate with the socket at her spine sitting empty and useless to bleed any of it off. He caps the pen, uncaps it again, gets comfortable on the crate. 'We've got all the time in the world,' he says, and puts his hand back between her legs. 'Again.'</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Climbed. You went up three just now. Nice steep little curve you've got.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> We've got all the time in the world. Again.</div></div>
<<set $game_state.current_canvas = "cell_use_charge_test">>
<<set $game_state.current_node = "cell_use_charge_test.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_charge_test.readout") === -1) { vn.push("cell_use_charge_test.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -10.0, true, null);setup.showEffectNotification();<</script>>
[[Let him finish writing.->Location_captive_room]]<img src="./videos/scenes/cell_inventory_the_order.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The door lets him in and no one else. He has a folding chair under one arm; he snaps it open a body-length from her and drops into it like a man come to look over stock he already owns. No hurry in him. He sits with his knees apart and looks her up and down the way you'd look at a crate you paid too much for and mean to get your money's worth out of, and lets the quiet sit until it's clear the quiet is his too.</p><p>She stands where the room keeps her. He takes his time getting to the point, because taking his time is part of it.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Take it off. All of it. Let's see what I paid for.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Slow. I've got a whole drawer of notes on you upstairs and not one of them tells me what a thing like you is worth. So I'll price you myself.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.base">>
<<script>>setup.markCanvasTriggered("cell_use_inventory");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.base") === -1) { vn.push("cell_use_inventory.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_inventory_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_inventory_strip_on_command.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Her hands don't go to the collar fast enough — a second's hesitation, a beat too long deciding whether to obey — and that's the whole of the excuse he wants. He comes up out of the chair and he's on her before the pause is done, gets a fist in the cloth at her shoulder and tears it down off her in one hard pull, seam giving with a rip, already grabbing the next piece before the first has hit the floor. He strips her the way you'd tear the wrap off stock you paid too much for and can't be bothered to unpick — rough, fast, cloth going in his hands — until she's bare and the torn scraps are around her feet.</p><p>He doesn't do it because he has to. He does it because making her do it herself is slower, and because tearing it off her tells her what she is here quicker than any order could — a thing to be unwrapped, not a thing to be asked. When she's bare he steps back to the chair, cock already out, and looks her over like he's checking he got what he paid for.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Too slow. When I say off, it comes off. I'll not ask a machine twice.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There. Bought and paid for, and not even worth the wrapping. Let's see what's under it.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.n2") === -1) { vn.push("cell_use_inventory.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_inventory_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_inventory_the_inspection.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He gets up only when she's bare. He checks her over like stock — turns her by the hip, spreads her open with both thumbs, works two fingers up into her cunt and a third into her ass in the same breath, no interest at all in whether she's ready for any of it.</p><p>She doesn't answer him, so he spits in her face, unhurried, right across the cheek, and wipes his wet hand off on her tits. Then his thumb finds the empty socket at the base of her spine, and he stops, and works the tip of it into the hole, and laughs.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Nice and tight for a thing that's been passed round half the docks. They tell me you were a fixture in a whorehouse. Doesn't feel used up. Yet.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There it is. That's where the party trick lived. Empty as a robbed till. Somebody's had it out of you — so now you're just holes and a bill.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.n3") === -1) { vn.push("cell_use_inventory.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_inventory_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_inventory_he_fucks_her_throat.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He doesn't ask her to suck it. He fists a handful of her hair, drags her down onto his cock, and fucks it straight down her throat — holds her there past the gag, past the point her eyes run and drool ropes off her chin onto her tits, then hauls her up for one wet breath and shoves her back down onto it.</p><p>He uses her face like it owes him money, both hands in her hair now, driving her throat down the length of him at his own pace and holding her there each time until her whole body kicks. She takes all of it, because taking it is the only thing left that she does.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There's a good hole.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> That's it. Choke on it. That part of you still works, at least.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.n4") === -1) { vn.push("cell_use_inventory.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_inventory_Node_n5">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_inventory.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He pulls her up off his cock, turns her, and folds her over the table with one hand flat between her shoulder blades, holding her down against nothing because she isn't fighting. Then he pushes into her cunt and fucks her the way a man saws a board he means to use — unhurried, all the way through, hand pressing her cheek to the wood.</p><p>When he spends himself in her he doesn't make a sound. The charge comes off him and goes into her and stops. It climbs up behind the plate under her sternum and stacks there against the wall of the stripped socket, turning over, looking for the way out, and not finding one.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> That's one in the tank. Now let's see if any of it goes anywhere.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.n5">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.n5") === -1) { vn.push("cell_use_inventory.n5"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_inventory_Node_n6">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_inventory_he_takes_her_ass.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He pulls out of her cunt still hard, works the head against her ass, and pushes in slow. It takes him longer here, and it's tighter, and he doesn't care. He holds her by the hips and drives up into her ass an inch from the empty socket at the base of her spine — the hole that used to be the drain, the thing that TOOK, that reached down a man's spine and pulled something loose.</p><p>Now it only fills her. Every stroke stacks the charge higher with nowhere for it to run, and when he empties himself into her ass she gets all of it and there is nothing on the far end of the take — no man gone soft and honest and hers — just his cum in her and the meter behind her sternum jumping.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> That used to be the good one, didn't it. The one that emptied a man out and left him yours. Look at it now — just another hole I get to use.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.n6">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.n6") === -1) { vn.push("cell_use_inventory.n6"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_inventory_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_inventory_he_finishes_the_read_out.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He stays buried in her ass a beat after he finishes, breathing, then pulls out and lets it run. He wipes his cock off on the curve of her ass and steps back to look at her — bent over his table, cheek on the wood, holding what he put in her.</p><p>The read-out on the plate under her sternum ticks up one, and does not tick back down. It never ticks back down now. He watches the number, not her; takes the notebook out of his coat, and writes it in the column beside the others.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There. Up, and holding. Whatever's meant to bleed that off isn't working — because I've got the part that does it sitting in a drawer upstairs. You just fill.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Good. We'll run it again once you've settled.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.readout") === -1) { vn.push("cell_use_inventory.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -10.0, true, null);setup.showEffectNotification();<</script>>
[[Get up off the table.->Location_captive_room]]<p>She comes up out of it slowly, which is new. There is a ceiling, painted a long time ago, and a light in a cage that does not turn off. A bed bolted to the floor. A chair that isn't. A table with nothing on it. In the middle of the floor there is a drain, because whoever built this room expected to hose it out.</p><p>She reaches back and finds the seam at the base of her spine open and empty. It doesn't hurt. That's the part she keeps checking — it doesn't hurt, the way a missing tooth doesn't hurt, and her hand goes back to it three times before she makes it stop.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> Charge comes in. Charge has to go somewhere.</div></div><p>There is a door. It is steel and newer than the room, and it has no handle on this side. She sits on the edge of the bed with her hands in her lap and listens to men moving on the other side of it, and she works out, without any particular feeling about it, that she can lie down or she can pay attention, and that those are the two things there are.</p>
<<set $game_state.current_canvas = "captivity_intro">>
<<set $game_state.current_node = "captivity_intro.base">>
<<script>>setup.markCanvasTriggered("captivity_intro");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("captivity_intro.base") === -1) { vn.push("captivity_intro.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Sit up.->Location_captive_room]]<img src="./videos/scenes/captivity_taken.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>It's the newspaper she notices, half a street back — folded the same way, held by the same patient man who stepped aside to let them carry her past. He doesn't hurry. He doesn't need to. The van is already at the kerb behind her and the dock is long and there is nowhere on it to go that isn't further out.</p><p>This time nobody bothers with the bag until she's inside. A hand finds the seam at the base of her spine and works it, patient, the way a man works a jar lid, until it gives. The weight of the drain comes off her. Somebody says something about the boss wanting a proper look this time, and somebody else laughs, and then the bag.</p>
<<set $game_state.current_canvas = "captivity_recovery">>
<<set $game_state.current_node = "captivity_recovery.base">>
<<script>>setup.markCanvasTriggered("captivity_recovery");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("captivity_recovery.base") === -1) { vn.push("captivity_recovery.base"); }<</script>>
<<nobr>>
<<link "Go under." "Location_captive_room">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "captivity_entered", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She doesn't hear the door. One second she's got the ledger under her arm and the stair in front of her; the next there's a bag over her head, a knee in her back, and hands that know exactly how much pressure a body takes before it stops fighting. Nobody says a word — professionals. The last thing she registers before the dark is a man at the foot of the stair folding his newspaper, the same patient watcher from The House, stepping aside to let them carry her past.</p><p>She comes to upright in a chair that isn't restraining her, which is its own kind of message. A back room, warm light, a door that's shut and not locked. Across a plain table a man watches her with the patience of someone who owns the building and everything in it — unhurried, certain, a voice that will never have to be raised because the room is already his.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Wren. That's the file Mercer keeps you under, so it's the one I'll use. You've been busy. Doss. Rourke. My man Marsh, who woke up Monday poorer and stupider and can't tell me why. A great deal of digging, for one of Mercer's assets — into people who are mine.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Don't reach for whatever cover you had ready. I knew you were Mercer's before you finished your first drink down here; I hear things before they're said. That isn't what interests me.</div></div><p>He leans in, unhurried, and doesn't threaten her — doesn't reach for anything. He looks at her the way you'd look at the first genuinely interesting thing to cross your desk in years.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> What interests me is that when my man watched you leave The House, he swore the girl who walked out wasn't quite a girl. And my people are very rarely wrong about the things that unsettle them. So before I decide what you're worth — to me, and to the people I answer to, who'd pay a great deal for the honest answer — I'll ask you plainly. What are you, really?</div></div><p>The question hangs. She has covers for men who want her body and men who want her obedience, and none at all for a man who wants the one thing under the chip she can't reach herself. Somewhere below the amputation, something turns over at being seen. She doesn't answer. He reads the not-answering, nods as though it told him plenty, and lifts two fingers — and the bag comes down again.</p><p>Hands under her arms. A stair, and then further down than a stair should go. The salt smell of the docks thins out and stops. Somewhere in the dark someone finds the seam at the base of her spine, working it the way a man works a jar lid, patient, until it gives — and the weight of the drain comes off her and does not come back. She goes under before she can count the turns.</p>
<<set $game_state.current_canvas = "hunt_the_grab">>
<<set $game_state.current_node = "hunt_the_grab.base">>
<<script>>setup.markCanvasTriggered("hunt_the_grab");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_the_grab.base") === -1) { vn.push("hunt_the_grab.base"); }<</script>>
<<nobr>>
<<link "Go under." "Location_captive_room">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "bastien_revealed", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "captivity_entered", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/scenes/crew_den_interior.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The key's under the third stair, like Marsh said. Inside, the rooms have the stillness of a place people left fast and never came back to — a card game abandoned mid-hand, a coat that fits no one now, a fridge that gave out weeks ago and was never opened since. Doss and Rourke lived here, and died somewhere else. She takes the place apart the way she takes everything apart: cold, thorough, patient.</p>
<<set $game_state.current_canvas = "hunt_crew_den_search">>
<<set $game_state.current_node = "hunt_crew_den_search.base">>
<<script>>setup.markCanvasTriggered("hunt_crew_den_search");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_crew_den_search.base") === -1) { vn.push("hunt_crew_den_search.base"); }<</script>>
<<nobr>>
<<link "Take the place apart." "Canvas_hunt_crew_den_search_Node_find">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Not yet." "Location_underworld_strip">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>It's in a strongbox behind the water heater — the one thing in the place worth locking. A run of ledgers, tallies of cargo that never touched a legal dock, and a name at the top of every page: the man the crew answered to, whose coin moved through Doss's hands and out into the company's blind spots. Not Cain. Someone she's never heard of. Bastien.</p><p>She has it now — a person at the end of the trail, a place to put the hunt. She pockets the ledger and turns for the stair.</p>
<<set $game_state.current_canvas = "hunt_crew_den_search">>
<<set $game_state.current_node = "hunt_crew_den_search.find">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_crew_den_search.find") === -1) { vn.push("hunt_crew_den_search.find"); }<</script>>
<<nobr>>
<<link "Bastien. Take the ledger and go." "Canvas_hunt_the_grab_Node_base">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "bastien_found", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/scenes/marsh_booking.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Marsh comes in on the hour, exact. A small neat man who checks the room's exits before he checks the girl, and takes the one the madam pointed him to without a word — her, tonight, in Rue's place. He doesn't notice the swap, or doesn't care; he wants the hour he paid for. Whatever a careful man keeps behind his face, she'll have to work him loose for it — and she knows the one act that opens him.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_false"}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "lt", "value": 1}]})>>
<p>The drain sits cold against her hip — spent on Renner, never recharged. She can take Marsh's hour for the coin, but the truth stays locked behind his face until the weapon's live. Recharge it at the cradle and take his next Sunday.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_false"}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]})>>
<p>She's carrying the emitter, not the drain — wrong weapon for this work. She can take Marsh's hour for the coin, but she'll get nothing out of him. Swap to the drain at her bench and take his next Sunday.</p>
<</if>>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.base">>
<<script>>setup.markCanvasTriggered("hunt_marsh_sunday");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.base") === -1) { vn.push("hunt_marsh_sunday.base"); }<</script>>
<<nobr>>
<<link "Take him — earn the hour." "Canvas_hunt_marsh_sunday_Node_oral">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Not tonight." "Location_underworld_strip">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/marsh_oral_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>On her knees, she takes him in her mouth and works him the way the trade taught her, but slower, watching — reading where the careful in him loosens and where it doesn't. He rests a hand in her hair, polite even here, and lets his head go back. Getting there.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.oral">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.oral") === -1) { vn.push("hunt_marsh_sunday.oral"); }<</script>>
<<nobr>>
<<link "Keep working him." "Canvas_hunt_marsh_sunday_Node_oral">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Climb on and ride him." "Canvas_hunt_marsh_sunday_Node_vaginal">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Turn around — give him your ass." "Canvas_hunt_marsh_sunday_Node_anal">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('hunt_marsh_sunday:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_hunt_marsh_sunday_Node_finish_soft">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hunt_marsh_sunday:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/marsh_ride_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She climbs on and rides him slow, keeping the pace hers, watching the careful man forget to be careful. He grips her hips and lets go of something — not much, but she's not after much. She's after the one thing men give up when she takes them the last way.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.vaginal">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.vaginal") === -1) { vn.push("hunt_marsh_sunday.vaginal"); }<</script>>
<<nobr>>
<<link "Keep riding him." "Canvas_hunt_marsh_sunday_Node_vaginal">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back down on your knees." "Canvas_hunt_marsh_sunday_Node_oral">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Turn around — give him your ass." "Canvas_hunt_marsh_sunday_Node_anal">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('hunt_marsh_sunday:cc1') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_hunt_marsh_sunday_Node_finish_soft">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hunt_marsh_sunday:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/marsh_anal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She turns and gives him her ass, and he takes it the way careful men take the thing they'd never ask for out loud — slow, greedy, undone by it. This is the act that opens him. Bent over the bed with his hands clamped on her hips, she waits for the moment his whole guarded weight goes soft, and reaches, if she's carrying it, for the drain.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.anal">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.anal") === -1) { vn.push("hunt_marsh_sunday.anal"); }<</script>>
<<nobr>>
<<link "Take it — work him to the edge." "Canvas_hunt_marsh_sunday_Node_anal">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back onto his cock." "Canvas_hunt_marsh_sunday_Node_vaginal">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}, {"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_false"}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('hunt_marsh_sunday:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass — take the drain." "Canvas_hunt_marsh_sunday_Node_drain">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hunt_marsh_sunday:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}, {"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_true"}]})>>
<span @class="setup.isChoiceVisited('hunt_marsh_sunday:cc3') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass." "Canvas_hunt_marsh_sunday_Node_finish_ass_paid">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hunt_marsh_sunday:cc3");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}, {"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_false"}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "lt", "value": 1}]})>>
<span @class="setup.isChoiceVisited('hunt_marsh_sunday:cc4') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass — the drain's spent." "Canvas_hunt_marsh_sunday_Node_finish_ass_nodrain">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hunt_marsh_sunday:cc4");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}, {"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_false"}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]})>>
<span @class="setup.isChoiceVisited('hunt_marsh_sunday:cc5') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass — you're carrying the wrong gear." "Canvas_hunt_marsh_sunday_Node_finish_ass_nodrain">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hunt_marsh_sunday:cc5");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><p>He finishes in her ass, and the drain wakes with him — the same act that hands her nothing with a paying man hands her everything with this one. The careful goes out of him all at once, loose and honest, his guarded weight gone soft against her back. She asks the only thing she came for: where the crew lived — Doss, Rourke, the rest. He tells her. A dead man's rooms down past the market, nobody's cleared them, the door still answers to a key under the third stair. In a few minutes he'll surface remembering a good hour and nothing else.</p><p>On her way out a man at the bar folds his paper and watches her go — not the madam's muscle, someone else's, patient about it. She clocks him clocking her and files it. Careful men keep careful company; someone just paid attention to the wrong girl asking the wrong questions.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.drain">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.drain") === -1) { vn.push("hunt_marsh_sunday.drain"); }<</script>>
<<nobr>>
<<link "Take the address and go." "Location_underworld_strip">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", 15.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drain_charge", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "crew_known", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>He finishes the way he came in — quiet, contained, already reaching for his coat before his breath's back. A careful man keeps his softness behind him when he doesn't lose the last of himself, and the mouth and the cunt don't cost him that. She takes the coin he leaves and lets him go. Whatever he knows is still behind his face. It's the ass that opens him — she'll need him again, and the drain worn and charged.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.finish_soft">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.finish_soft") === -1) { vn.push("hunt_marsh_sunday.finish_soft"); }<</script>>
<<script>>advanceTime(120);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "coin", "add", 15.0, false, null);setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);setup.showEffectNotification();<</script>>
[[Clean up. Collect.->Location_underworld_brothel]]<p>He finishes in her ass and goes slack against her, spent and honest and empty of anything she still needs — she took what was behind his face already, and a man only has the one secret worth the drain. Now he's just a careful regular with a standing Sunday, tipping fair, none the wiser. She straightens, takes the coin, and lets him go.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.finish_ass_paid">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.finish_ass_paid") === -1) { vn.push("hunt_marsh_sunday.finish_ass_paid"); }<</script>>
<<script>>advanceTime(120);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "coin", "add", 15.0, false, null);setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);setup.showEffectNotification();<</script>>
[[Clean up. Collect.->Location_underworld_brothel]]<p>He finishes in her ass and the careful goes out of him — loose, open, everything he knows a touch away — and she's got nothing on her to take it with. Without the drain worn and charged he's just a spent john with a soft face and a secret still locked behind it. She takes his coin and files the miss. Recharge the drain at the cradle, carry it, take his Sunday again — the truth's a touch away once the weapon's on her.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.finish_ass_nodrain">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.finish_ass_nodrain") === -1) { vn.push("hunt_marsh_sunday.finish_ass_nodrain"); }<</script>>
<<script>>advanceTime(120);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "coin", "add", 15.0, false, null);setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);setup.showEffectNotification();<</script>>
[[Clean up. Collect.->Location_underworld_brothel]]<p>Sunday, and the House keeps its one clock. Rue's at her chair with the ledger open to tonight's page, the far room already blocked out in her own hand. A few hours yet before the careful little man walks in expecting her.</p><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Your Sunday standing. The careful little man, same hour every week. I want tonight.</div></div><p>A slow look up from the ledger, reassessing. She knows exactly who the girl means, and she doesn't ask why.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/rue.jpg" alt="Rue" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Rue:</strong> Marsh. Quiet money, tips fair, never any trouble — the kind of regular you don't hand off. But I could use a night that's mine, and you've got the look of someone who wants him for more than the coin.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/rue.jpg" alt="Rue" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Rue:</strong> So take tonight. Tonight, mind — not my Sundays. He's on the hour, the far room. Whatever your business is, keep it off my floor, and he never hears from me that the girl got swapped. You want him again, you come ask me again.</div></div>
<<set $game_state.current_canvas = "hunt_marsh_scheme">>
<<set $game_state.current_node = "hunt_marsh_scheme.base">>
<<script>>setup.markCanvasTriggered("hunt_marsh_scheme");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_scheme.base") === -1) { vn.push("hunt_marsh_scheme.base"); }<</script>>
<<nobr>>
<<link "Tonight, then. The far room." "Location_underworld_brothel">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "rue_met", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Not this week." "Location_underworld_brothel">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The floor's quiet this hour — a girl asleep in a chair, a lamp left burning low, the rooms all shut. The madam runs the House by day; nothing moves through here until she's back at her post. Come back when she's in, if it's work you're after.</p>
<<set $game_state.current_canvas = "brothel_offhours">>
<<set $game_state.current_node = "brothel_offhours.base">>
<<script>>setup.markCanvasTriggered("brothel_offhours");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("brothel_offhours.base") === -1) { vn.push("brothel_offhours.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Head back up.->Location_underworld_strip]]<p>Rue keeps her chair by the rooms, ledger open, watching the floor the way a dealer watches a table. She nods when she sees her — not warmth, just the level look of one working woman clocking another.</p>
<<set $game_state.current_canvas = "rue_hub">>
<<set $game_state.current_node = "rue_hub.base">>
<<script>>setup.markCanvasTriggered("rue_hub");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rue_hub.base") === -1) { vn.push("rue_hub.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "brothel_hired", "operator": "is_false"}]})>>
<span @class="setup.isChoiceVisited('rue_hub:cc0') ? '' : 'unlocked-choice'">
<<link "Ask her for work." "Canvas_rue_hub_Node_work">><<script>>setup.markChoiceVisited("rue_hub:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "brothel_hired", "operator": "is_true"}]})>>
<span @class="setup.isChoiceVisited('rue_hub:cc1') ? '' : 'unlocked-choice'">
<<link "Trade a word." "Canvas_rue_hub_Node_talk">><<script>>setup.markChoiceVisited("rue_hub:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<link "Leave her to it." "Location_underworld_brothel">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> I want the work. The rooms.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/rue.jpg" alt="Rue" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Rue:</strong> Course you do. They all do, once they've been down here long enough to stop pretending otherwise.</div></div><p>She writes something in the ledger without looking up — a name, or a mark against one — and tips her head at the rooms. That's the whole hiring. No contract, no questions.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/rue.jpg" alt="Rue" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Rue:</strong> Rooms are yours when you want them. Coin's yours after. Don't make me regret the chair I didn't get up from.</div></div>
<<set $game_state.current_canvas = "rue_hub">>
<<set $game_state.current_node = "rue_hub.work">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rue_hub.work") === -1) { vn.push("rue_hub.work"); }<</script>>
<<nobr>>
<<link "You won't." "Canvas_rue_hub_Node_base">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "brothel_hired", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She trades what she hears, the small weather of the underworld — which men are talking, whose protection got thinner this week, who's asking after who. "You didn't get it off me," she says every time, and tells her anyway. It isn't much. But it's a face down here that isn't a mark and isn't an owner, and there aren't many of those.</p>
<<set $game_state.current_canvas = "rue_hub">>
<<set $game_state.current_node = "rue_hub.talk">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rue_hub.talk") === -1) { vn.push("rue_hub.talk"); }<</script>>
<<nobr>>
<<link "Good talking." "Location_underworld_brothel">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/scenes/rue_madam.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The woman by the rooms takes her measure before she's three steps in — the flat, unhurried read of someone who's priced a thousand bodies and is rarely wrong by much. She doesn't get up.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/rue.jpg" alt="Rue" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Rue:</strong> Rue. I run this house. Not the muscle, not the owner — the floor. Which means I decide who works my rooms and who serves the few names worth keeping. You've got the look of someone who's been used for it before and stopped feeling it. Down here that's not a mark against you. That's the job.</div></div><p>She tips her head at the rooms behind her — the offer plain, no contract, no company, nobody signing the girl out at the end of it.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/rue.jpg" alt="Rue" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Rue:</strong> Coin's yours when you're done, and nobody up top hears a word of it from me. You want in, you ask me. Simple as that.</div></div>
<<set $game_state.current_canvas = "rue_intro">>
<<set $game_state.current_node = "rue_intro.base">>
<<script>>setup.markCanvasTriggered("rue_intro");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rue_intro.base") === -1) { vn.push("rue_intro.base"); }<</script>>
<<nobr>>
<<link "Take her measure back." "Location_underworld_brothel">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "rue_introduced", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She slides onto the stool beside him and it's wrong before she's said a word. Out of the coveralls, in her own clean clothes, she's not the dock-rat who hauls his crates — she's a stranger sitting too close, and his bleary eyes narrow on her with the old wariness coming up through the drink.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> ...do I know you? You don't look like you tonight.</div></div><p>Even three drinks down he knows the shape of her is off. Nothing she wants from him happens while she's wearing the wrong face. The cover's back in her quarters, where it does her no good.</p>
<<set $game_state.current_canvas = "react_renner_nocover_anchor">>
<<set $game_state.current_node = "react_renner_nocover_anchor.base">>
<<script>>setup.markCanvasTriggered("react_renner_nocover_anchor");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("react_renner_nocover_anchor.base") === -1) { vn.push("react_renner_nocover_anchor.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Leave before he sobers on it.->Location_the_anchor]]<p>She comes onto the floor and Renner's head comes up wrong. The coveralls aren't on her — she's in her own clothes, the clean expensive line of them all out of place against the racks — and for a second he doesn't place her at all. Then the wariness: not recognition. Suspicion.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> ...the hell are you dressed like that for. Do I know you?</div></div><p>Wrong skin for this yard. Whatever she'd get done here today, she won't get it done looking like this. The cover's back in her quarters; it does her no good on the rack.</p>
<<set $game_state.current_canvas = "react_renner_nocover_depot">>
<<set $game_state.current_node = "react_renner_nocover_depot.base">>
<<script>>setup.markCanvasTriggered("react_renner_nocover_depot");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("react_renner_nocover_depot.base") === -1) { vn.push("react_renner_nocover_depot.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Get out before he looks harder.->Location_renner_depot]]<img src="./videos/scenes/renner_anchor.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Renner's at the end of the bar. She takes the stool and starts in — heard he's rebuilding, looking for work — and watches it land wrong. She's too clean. Too put-together for a woman who'd want a dock-rat's wages. He looks at her the way a man with enemies looks at a stranger who knows his business.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Whatever you are — cop, fed, one of theirs — I've got nothing to say to it. Off my stool.</div></div><p>He turns his shoulder and that's the end of it. Dressed like this she's not cheap hands; she's a question, and a man this far down doesn't answer questions. She'd have to come at him as something that doesn't get a second look — the cover's in her quarters, and it does her no good on the rack.</p>
<<set $game_state.current_canvas = "react_renner_threat">>
<<set $game_state.current_node = "react_renner_threat.base">>
<<script>>setup.markCanvasTriggered("react_renner_threat");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("react_renner_threat.base") === -1) { vn.push("react_renner_threat.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Leave it.->Location_the_anchor]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_anal_once", "operator": "is_true"}]})>>
<video src="./videos/sex/renner_cheerup_alley_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He's wrecked tonight, and she does the thing that works. She gets him out of the bar and into the dark behind it and gives him whatever he needs — on her knees on the filthy concrete, or bent over a crate, the cold boss falling apart and the help putting him back together with her mouth and her ass. It's the one real thing she has, and she spends it on him like it's nothing, because it is.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_oral_once", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "renner_anal_once", "operator": "is_false"}]})>>
<video src="./videos/sex/renner_cheerup_oral_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He's low tonight. She slips off the stool and into the dark beside the cigarette machine and takes his cock in her mouth right there, fast and devoted, the bartender pointedly not looking. He comes with his hand in her hair saying her work-name like a man saying his prayers wrong.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_oral_once", "operator": "is_false"}]})>>
<p>He's low tonight — too low to do anything but sit in it. She sits closer than she needs to, lets her thigh press his, lets her hand rest high on his leg where the wanting has somewhere to go. It's not much. But it makes the worst of the night about her instead of about the wreck of his life, and that's the seed of everything that comes after.</p>
<</if>>
<<set $game_state.current_canvas = "amb_renner_cheerup">>
<<set $game_state.current_node = "amb_renner_cheerup.base">>
<<script>>setup.markCanvasTriggered("amb_renner_cheerup");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("amb_renner_cheerup.base") === -1) { vn.push("amb_renner_cheerup.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Leave him steadier.->Location_the_anchor]]<img src="./videos/scenes/amb_renner_anchor_ruin.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 20}]})>>
<p>Two stools down he's telling the bartender a story about the depot before Cain — when it was something, when he was something. The bartender's heard it. So has she. Renner doesn't notice either of them stopped listening a while ago.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 20}, {"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 40}]})>>
<p>There's a letter on the bar in front of him tonight he keeps not reading — blacklist stationery, the third this month. He drinks around it. When he catches her looking he turns it face-down, too late, and orders another.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 40}]})>>
<p>He's further gone tonight than she's seen him — talking too much, too loud, the rot all the way to the surface. What they made him part of. What burned. What he signed for and told himself he didn't understand. He won't remember saying any of it tomorrow. She'll remember all of it. It's the guilt the seduction never has to touch.</p>
<</if>>
<<set $game_state.current_canvas = "amb_renner_anchor_ruin">>
<<set $game_state.current_node = "amb_renner_anchor_ruin.base">>
<<script>>setup.markCanvasTriggered("amb_renner_anchor_ruin");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("amb_renner_anchor_ruin.base") === -1) { vn.push("amb_renner_anchor_ruin.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Let him drink.->Location_the_anchor]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 20}]})>>
<p>She's halfway up a ladder with a crate when she feels it — Renner, stopped at the bottom, not pretending to check a manifest, just watching her work. He looks away when she looks down. But he was watching, and now they both know he was.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 20}, {"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 40}]})>>
<p>He finds reasons to be where she is — stands too close at the racks, hands her tools she didn't ask for, lets the backs of his fingers find her arm. "You're good at this," he says, like it costs him, and doesn't move away when he's said it.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 40}]})>>
<p>He doesn't bother with the pretext anymore. He takes the crate out of her hands, sets it down, and backs her into the shelving with his whole ruined weight against her. "Leave it," he says against her neck. "The invoices can wait. Come in the back."</p>
<</if>>
<<set $game_state.current_canvas = "walkin_renner_depot">>
<<set $game_state.current_node = "walkin_renner_depot.base">>
<<script>>setup.markCanvasTriggered("walkin_renner_depot");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("walkin_renner_depot.base") === -1) { vn.push("walkin_renner_depot.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[...->Location_renner_depot]]<p>He'll answer anything now, and remember none of it. She crouches to his eye level and asks, quiet, the way you'd read a meter.</p><span id="cascade-b1-beat-0"><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> The gear you sold Vance. What did it build?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Containment. Restraint rigs, suppression fields, a wet lab built to hold things that aren't supposed to be holdable. I didn't ask what. They paid on time and I had a business to save.</div></div><span id="cascade-b1-beat-1"><<linkreplace "What did Cain take?">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> When Cain hit the facility. What did he take?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Nothing. He freed it. Whatever they were holding in there, he let it out before he burned the place. The manifests after didn't add up — they were short bodies that weren't bodies. He wasn't robbing them. He was emptying a cage.</div></div><p>She files it somewhere the cradle can't reach. The monster she's sent to kill broke into the worst place these people built and let the prisoners walk out. It doesn't fit the briefing. Nothing about him fits the briefing, and the not-fitting is starting to have a shape.</p><span id="cascade-b1-beat-2"><<linkreplace "Why did Cain burn you down?">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Cain didn't just walk away from you. He came for you. Why?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Two of my people. Doss and Rourke. They moved my materials — and they were moving them to him too, feeding Cain off my own floor. When I found out I didn't just cut them loose. I did them, and everyone in their houses, the same night. He burned me to the slab for it. We've been at each other since.</div></div><p>She keeps her face a meter-reader's blank. So the burned yard she's been picking through wasn't collateral in someone else's war — it was the answer, and the two files she pulled out there are the men he's confessing to now. The wreck of him has a shape after all, and the shape is a debt.</p><span id="cascade-b1-beat-3"><<linkreplace "Where is he now?">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Where would he go to ground?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> I don't know. But those two came up out of the underworld before they ever came to me — that's where their kind runs. If Cain's anywhere, he's down there. That's all I... that's all I have.</div></div><span id="cascade-b1-beat-4"><<linkreplace "Tell him to forget.">><p>She tells him to forget, and he will. In a few minutes he'll surface blinking, his cock going soft and no memory of the last ten, certain only that the new hire is very good and he should keep her close. She fixes her clothes while the man who just handed her everything stares at nothing.</p>[[Leave him to come back to himself.->Location_renner_depot]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "renner_control_canvas">>
<<set $game_state.current_node = "renner_control_canvas.intro">>
<<script>>setup.markCanvasTriggered("renner_control_canvas");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("renner_control_canvas.intro") === -1) { vn.push("renner_control_canvas.intro"); }<</script>>
<<script>>advanceTime(3);<</script>><p>He goes soft and open the second she's done, the controls warm in her hand again — hers for a few minutes, and he won't remember a second of it. She already has what he knew. There's nothing left in him she needs; there's only what she feels like taking.</p>
<<set $game_state.current_canvas = "renner_control_canvas">>
<<set $game_state.current_node = "renner_control_canvas.again">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("renner_control_canvas.again") === -1) { vn.push("renner_control_canvas.again"); }<</script>>
<<nobr>>
<<link "Ask him again." "Canvas_renner_control_canvas_Node_again_ask">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Just leave him." "Location_renner_depot">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She crouches to his eye level and asks anyway — the gear, the two he buried, where the man went.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Two of mine — Doss and Rourke — feeding him off my floor. I did them and theirs, and he burned me for it. Where he went — down. It's all down in the underworld. That's everything I had. I swear it.</div></div><p>The same confession, in the same order — but this time she keeps the two names. He doesn't know he's said it before, and he never will. Whatever she already had, watching the wrecked man empty himself out for her on command is its own kind of confirmation. She fixes her clothes and leaves him to surface alone.</p>
<<set $game_state.current_canvas = "renner_control_canvas">>
<<set $game_state.current_node = "renner_control_canvas.again_ask">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("renner_control_canvas.again_ask") === -1) { vn.push("renner_control_canvas.again_ask"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Leave him to come back to himself.->Location_renner_depot]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 0}]})>>
<p>He finishes on her face with a groan that's half a sob, one hand cradling the back of her head like she's something he can't believe he's allowed. He'd give her anything right now. He doesn't know yet how much that's going to cost him.</p><video src="./videos/sex/renner_finish_facial_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 1}]})>>
<p>He finishes inside her, holding her down on him, shaking. For a moment after he just breathes into her shoulder, wrecked and grateful, and she lets him have the moment because letting him have it is how she keeps him.</p><video src="./videos/sex/renner_finish_inside_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}]})>>
<p>He finishes in her ass, and the controls slide warm into her hand — the give, the opening, his whole ruined weight gone pliant against her back. The same act that hands her nothing with Mercer hands her everything with him.</p><video src="./videos/sex/renner_finish_anal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<</if>>
<<set $game_state.current_canvas = "loop_renner_finisher">>
<<set $game_state.current_node = "loop_renner_finisher.climax">>
<<script>>setup.markCanvasTriggered("loop_renner_finisher");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_renner_finisher.climax") === -1) { vn.push("loop_renner_finisher.climax"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "lt", "value": 2}]})>>
<span @class="setup.isChoiceVisited('loop_renner_finisher:cc0') ? '' : 'unlocked-choice'">
<<link "Straighten up." "Location_renner_depot">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_renner_finisher:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "drains_done", "operator": "lt", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('loop_renner_finisher:cc1') ? '' : 'unlocked-choice'">
<<link "Take the keys." "Canvas_renner_control_canvas_Node_intro">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drains_done", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drain_charge", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "names_known", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_drained", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_leads_extracted", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_renner_finisher:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "drains_done", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('loop_renner_finisher:cc2') ? '' : 'unlocked-choice'">
<<link "Take the keys." "Canvas_renner_control_canvas_Node_again">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drains_done", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drain_charge", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "names_known", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_renner_finisher:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "lt", "value": 1}]})>>
<span @class="setup.isChoiceVisited('loop_renner_finisher:cc3') ? '' : 'unlocked-choice'">
<<link "Let it go — the drain's empty. Recharge it at the cradle." "Location_renner_depot">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_renner_finisher:cc3");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]})>>
<span @class="setup.isChoiceVisited('loop_renner_finisher:cc4') ? '' : 'unlocked-choice'">
<<link "Let it go — wrong gear." "Location_renner_depot">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_renner_finisher:cc4");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "lt", "value": 2}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "drains_done", "operator": "lt", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "drains_done", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "lt", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]})))>>
<<script>>
try {
window._engineNoExitsDiag_loop_renner_finisher = [{"text": "Straighten up.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "lt", "value": 2}]}}, {"text": "Take the keys.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "drains_done", "operator": "lt", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]}}, {"text": "Take the keys.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "drains_done", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]}}, {"text": "Let it go \u2014 the drain's empty. Recharge it at the cradle.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "lt", "value": 1}]}}, {"text": "Let it go \u2014 wrong gear.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "loop_renner_finisher",
choices: window._engineNoExitsDiag_loop_renner_finisher,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_loop_renner_finisher>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas loop_renner_finisher</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Navigation]]
<</if>>
<</nobr>><video src="./videos/sex/renner_loop_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She locks the office door. He's already reaching for her — the cold boss gone, just a wrecked man who's spent all day waiting for the help to come and take him apart.</p>
<<set $game_state.current_canvas = "loop_renner_office_sex">>
<<set $game_state.current_node = "loop_renner_office_sex.intro">>
<<script>>setup.markCanvasTriggered("loop_renner_office_sex");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_renner_office_sex.intro") === -1) { vn.push("loop_renner_office_sex.intro"); }<</script>>
<<nobr>>
<<link "Get on your knees." "Canvas_loop_renner_office_sex_Node_base_oral_r">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/renner_loop_oral_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>On her knees between the chair and the desk, his cock in her mouth, his hand gentler in her hair than Mercer's hand has ever been. Renner holds on like he's afraid she'll stop.</p>
<<set $game_state.current_canvas = "loop_renner_office_sex">>
<<set $game_state.current_node = "loop_renner_office_sex.base_oral_r">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_renner_office_sex.base_oral_r") === -1) { vn.push("loop_renner_office_sex.base_oral_r"); }<</script>>
<<nobr>>
<<link "Keep sucking his cock." "Canvas_loop_renner_office_sex_Node_base_oral_r">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Climb on his cock." "Canvas_loop_renner_office_sex_Node_base_vaginal_r">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('loop_renner_office_sex:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_loop_renner_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_renner_office_sex:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/renner_loop_vaginal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She rides him in the office chair, slow, watching the last of his composure go. He says things he'll be ashamed of — please, and the false name she works under, and don't stop, don't ever stop.</p>
<<set $game_state.current_canvas = "loop_renner_office_sex">>
<<set $game_state.current_node = "loop_renner_office_sex.base_vaginal_r">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_renner_office_sex.base_vaginal_r") === -1) { vn.push("loop_renner_office_sex.base_vaginal_r"); }<</script>>
<<nobr>>
<<link "Take him." "Canvas_loop_renner_office_sex_Node_base_vaginal_r">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back down on his cock." "Canvas_loop_renner_office_sex_Node_base_oral_r">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 50}, {"type": "clothing_item", "item_id": "cover_dockhand", "operator": "equipped"}]})>>
<span @class="setup.isChoiceVisited('loop_renner_office_sex:cc1') ? '' : 'unlocked-choice'">
<<link "Turn around — give him your ass." "Canvas_loop_renner_office_sex_Node_base_doggy_r">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_anal_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_renner_office_sex:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Not yet — he has to be all the way broken first.">Not yet — he has to be all the way broken first.</span><br>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('loop_renner_office_sex:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_loop_renner_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_renner_office_sex:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/renner_loop_doggy_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Bent over his own desk, his cock in her ass, his hands shaking on her hips. This is the act that hands her the keys — and now that she's turned it on him once, she can take them again whenever she likes.</p>
<<set $game_state.current_canvas = "loop_renner_office_sex">>
<<set $game_state.current_node = "loop_renner_office_sex.base_doggy_r">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_renner_office_sex.base_doggy_r") === -1) { vn.push("loop_renner_office_sex.base_doggy_r"); }<</script>>
<<nobr>>
<<link "Take it in the ass." "Canvas_loop_renner_office_sex_Node_base_doggy_r">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back on his cock." "Canvas_loop_renner_office_sex_Node_base_vaginal_r">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('loop_renner_office_sex:cc3') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass." "Canvas_loop_renner_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_renner_office_sex:cc3");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/ma_renner_blowjob_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Next afternoon, the office, like nothing happened — except she won't let it be like nothing happened. She waits until he's mid-sentence about a shipment, then asks him level, watching his face:</p><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong><<print $player.name>>:</strong> Did you like it. Yesterday. Or not.</div></div><p>He can't get out of it. She watches him try — the bluster, the deflection, the reach for the cold he used to have — and watches all of it fail, because the answer's already on him, plain. "You know I did," he says at last, like it's dragged out of him. And that's the thing she came for: not the yes, the surrender in having to say it. He's further in than he was, and he can feel it too.</p>
<<set $game_state.current_canvas = "ma_renner_blowjob">>
<<set $game_state.current_node = "ma_renner_blowjob.base">>
<<script>>setup.markCanvasTriggered("ma_renner_blowjob");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("ma_renner_blowjob.base") === -1) { vn.push("ma_renner_blowjob.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("npc", "npc_renner", "corruption", "add", 3.0, false, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "ma_blowjob_seen", "set");<</script>>
[[Leave it sitting on him.->Location_renner_depot]]<video src="./videos/sex/renner_blowjob_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>His wanting runs ahead of him all day now. She picks the afternoon — the door shut, the yard empty, his hands shaking around a pen that isn't writing anything. She crosses to him and goes down on her knees in front of the chair, and for once he doesn't pretend he doesn't know what's happening.</p><span id="cascade-b2-beat-0"><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> ...you don't have to.</div></div><p>It's the first kind thing he's said to her, and it's a lie, and they both let it be one. She takes his cock out and takes him in her mouth, and the cold boss makes a sound like something giving way under load.</p><span id="cascade-b2-beat-1"><<linkreplace "Work him.">><p>She works him slow, the way she's learned men like him want it — not skilled exactly, but devoted, like he's the only thing in the room worth her mouth. He finishes with his fist in her hair and the false name she's working under coming out of him broken in half. She swallows, and clocks the shape of what just cracked in him.</p>[[Wipe your mouth, get back to work.->Location_renner_depot]]
<</linkreplace>></span></span>
<<set $game_state.current_canvas = "cap_renner_blowjob">>
<<set $game_state.current_node = "cap_renner_blowjob.base">>
<<script>>setup.markCanvasTriggered("cap_renner_blowjob");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_renner_blowjob.base") === -1) { vn.push("cap_renner_blowjob.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "renner_oral_once", "set");<</script>><video src="./videos/scenes/rung_renner_grope_t4.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She stops pretending the contact is accidental — a hand on his thigh while he signs something, sliding up, letting him feel that she means it.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 30}]})>>
<p>He goes still. The pen stops. For a second the gutted business and the blacklist and the drinking are gone and there's just her hand and his breathing.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> ...don't stop.</div></div><p>Then he catches himself and pulls back, and hates that he had to.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 30}]})>>
<p>He doesn't pretend anymore. He covers her hand with his and presses it harder against him, his head going back, the cold boss just a man in a chair letting the help take him apart.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Whatever this is. Don't — don't stop doing it.</div></div>
<</if>>
<<set $game_state.current_canvas = "rung_renner_grope">>
<<set $game_state.current_node = "rung_renner_grope.base">>
<<script>>setup.markCanvasTriggered("rung_renner_grope");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rung_renner_grope.base") === -1) { vn.push("rung_renner_grope.base"); }<</script>>
<<script>>advanceTime(180);<</script>>
[[Back to the invoices.->Location_renner_depot]]<video src="./videos/scenes/rung_renner_flash_t3.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She's wearing nothing under the coveralls today, and she makes sure the moment comes where he can't not see it — bending slow for a low crate while he's looking.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 20}]})>>
<p>He looks. He looks longer than a man pretending not to. Then he turns hard to the window.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Get back to work.</div></div><p>His voice has gone rough. He doesn't mean the window.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 20}]})>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Christ. Come here.</div></div><p>He says it low, half to himself, and doesn't finish the order — doesn't know what he'd do with her if she came. The wanting's ahead of the nerve. But it's there now, plain, and they both felt it land.</p>
<</if>>
<<set $game_state.current_canvas = "rung_renner_flash">>
<<set $game_state.current_node = "rung_renner_flash.base">>
<<script>>setup.markCanvasTriggered("rung_renner_flash");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rung_renner_flash.base") === -1) { vn.push("rung_renner_flash.base"); }<</script>>
<<script>>advanceTime(180);<</script>>
[[Back to the invoices.->Location_renner_depot]]<video src="./videos/scenes/rung_renner_tease_t2.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>In the office now, the door shut. She works close — leans across him for a file she doesn't need, lets her body say the thing her mouth won't.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 10}]})>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Filing's in the cabinet. Not on my desk.</div></div><p>He doesn't look up. But he moves his chair back an inch, and the inch costs him something.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 10}]})>>
<p>His eyes go to her and snap away, and a muscle works in his jaw.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> ...You do that on purpose.</div></div><p>It isn't quite a question, and he doesn't tell her to stop.</p>
<</if>>
<<set $game_state.current_canvas = "rung_renner_tease">>
<<set $game_state.current_node = "rung_renner_tease.base">>
<<script>>setup.markCanvasTriggered("rung_renner_tease");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rung_renner_tease.base") === -1) { vn.push("rung_renner_tease.base"); }<</script>>
<<script>>advanceTime(180);<</script>>
[[Back to the invoices.->Location_renner_depot]]<img src="./videos/scenes/renner_office.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>It takes a week of good work before he stops finding fault just to have something to say to her. Then one grey afternoon he jerks his head at the glassed-in box at the back of the yard — his office — and doesn't quite look at her when he says it.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Invoices are three months behind and I can't read my own writing. You'll do them in here. Heater works, door shuts, and I don't have to watch the help breathe.</div></div><p>It's the closest thing to trust a man this gutted has left to give. The door clicks shut behind her, and the public yard with its cameras and its cold becomes, for the length of a workday, a small private room with one wrecked man in it. Everything she's here to do starts now.</p>
<<set $game_state.current_canvas = "cap_renner_noticed">>
<<set $game_state.current_node = "cap_renner_noticed.base">>
<<script>>setup.markCanvasTriggered("cap_renner_noticed");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_renner_noticed.base") === -1) { vn.push("cap_renner_noticed.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "renner_office_open", "set");<</script>>
[[Get to work.->Location_renner_depot]]<<set _sub_target = setup.checkAndSubstituteCanvas("hub_anchor_renner")>><<if _sub_target>><<goto _sub_target>><</if>>
<img src="./videos/scenes/renner_anchor_evening.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Renner's at his end of the bar, working on the evening's ruin. Off the clock he's quieter and worse — less temper, more rot. He tolerates her sitting near him now, which is its own kind of progress.</p>
<<set $game_state.current_canvas = "hub_anchor_renner">>
<<set $game_state.current_node = "hub_anchor_renner.base">>
<<script>>setup.markCanvasTriggered("hub_anchor_renner");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_anchor_renner.base") === -1) { vn.push("hub_anchor_renner.base"); }<</script>>
<<nobr>>
<<link "Sit with him a while." "Canvas_hub_anchor_renner_Node_sit">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Leave." "Location_the_waterfront">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She sits. He doesn't talk and doesn't tell her to go. The bartender refills him without being asked. Somewhere in the quiet she clocks what she sees — the blacklist letters he doesn't hide anymore, the way his hands shake until the second drink — and lets him think it's company.</p>
<<set $game_state.current_canvas = "hub_anchor_renner">>
<<set $game_state.current_node = "hub_anchor_renner.sit">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_anchor_renner.sit") === -1) { vn.push("hub_anchor_renner.sit"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Head out.->Location_the_anchor]]<<set _costs to [{"trait": "energy", "value": 15}]>>\
<<if setup.checkCostsAffordable(_costs)>>\
<<script>>setup.deductCosts("work_depot_haul");<</script>>
<<set _sub_target = setup.checkAndSubstituteCanvas("work_depot_haul")>><<if _sub_target>><<goto _sub_target>><</if>>
<img src="./videos/activities/renner_menial.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She works the floor — hauling, sorting, logging crates against a manifest that hasn't matched reality since Cain came through. It's dull, and her body doesn't tire the way a real one would, so she's better at it than the last three hands he ran off. The pay is thin, counted out grudging. But she does it well, and well is a language even a man this far gone still reads.</p>
<<set $game_state.current_canvas = "work_depot_haul">>
<<set $game_state.current_node = "work_depot_haul.base">>
<<script>>setup.markCanvasTriggered("work_depot_haul");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("work_depot_haul.base") === -1) { vn.push("work_depot_haul.base"); }<</script>>
<<script>>advanceTime(120);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("npc", "npc_renner", "relation", "add", 3.0, false, null);setup.applyAndNotifyTrait("player", null, "money", "add", 8.0, false, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "worked_renner_once", "set");<</script>>
[[Knock off for now.->Location_renner_depot]]
<<else>>
<div class="cost-blocked-message">
<p><<= setup.getCostBlockedMessage(_costs)>></p>
</div>
[[Back->Location_renner_depot]]
<</if>><<set _sub_target = setup.checkAndSubstituteCanvas("hub_depot_floor")>><<if _sub_target>><<goto _sub_target>><</if>>
<img src="./videos/scenes/renner_depot_floor.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Renner's depot. Half-empty racks, a forklift nobody's fixed, crates Cain's people left behind. He works the floor in a temper, and when he notices her at all it's to find fault. The work is the work.</p>
<<set $game_state.current_canvas = "hub_depot_floor">>
<<set $game_state.current_node = "hub_depot_floor.base">>
<<script>>setup.markCanvasTriggered("hub_depot_floor");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_depot_floor.base") === -1) { vn.push("hub_depot_floor.base"); }<</script>>
<<nobr>>
<<link "Check in with him." "Canvas_hub_depot_floor_Node_check_in">><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_office_open", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 0}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('hub_depot_floor:cc0') ? '' : 'unlocked-choice'">
<<link "Tease him." "Canvas_rung_renner_tease_Node_base">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "corruption", "add", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "arousal", "add", 3.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hub_depot_floor:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Tease him.">Tease him. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<<else>>
<span class="locked-choice" title="Tease him.">Tease him.</span><br>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_office_open", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 10}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('hub_depot_floor:cc1') ? '' : 'unlocked-choice'">
<<link "Flash him — work commando." "Canvas_rung_renner_flash_Node_base">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "corruption", "add", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "arousal", "add", 4.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_flirts_back", "set");<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hub_depot_floor:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Flash him — work commando.">Flash him — work commando. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<<else>>
<span class="locked-choice" title="Flash him — work commando.">Flash him — work commando.</span><br>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_office_open", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "renner_flirts_back", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 20}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('hub_depot_floor:cc2') ? '' : 'unlocked-choice'">
<<link "Get your hands on him." "Canvas_rung_renner_grope_Node_base">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "corruption", "add", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "arousal", "add", 5.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hub_depot_floor:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Get your hands on him.">Get your hands on him. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<<else>>
<span class="locked-choice" title="Get your hands on him.">Get your hands on him.</span><br>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_office_open", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "renner_oral_once", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 40}]})>>
<span @class="setup.isChoiceVisited('hub_depot_floor:cc3') ? '' : 'unlocked-choice'">
<<link "Take him in the office." "Canvas_loop_renner_office_sex_Node_intro">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_fucked_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("hub_depot_floor:cc3");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Take him in the office.">Take him in the office.</span><br>
<</if>>
<<link "Head out." "Location_the_waterfront">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She checks in. He grunts something about the manifest and doesn't look up. It's not much — but she lets him not-look, lets him get used to the shape of her in his yard. That's the work under the work.</p>
<<set $game_state.current_canvas = "hub_depot_floor">>
<<set $game_state.current_node = "hub_depot_floor.check_in">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_depot_floor.check_in") === -1) { vn.push("hub_depot_floor.check_in"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Back to it.->Location_renner_depot]]<img src="./videos/scenes/renner_anchor.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The Anchor is the kind of bar that's given up on its own neon. Renner's at the end of it — a big man gone soft and sour, three drinks into the evening and looking like the fourth won't help. He doesn't look up when she takes the stool beside him.</p><span id="cascade-b2-beat-0"><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Whatever you're selling, I'm broke. Whatever you're offering, I've had it.</div></div><p>She tells him she heard he's rebuilding. That she works cheap, doesn't ask questions, doesn't tire. He looks at her then — not the way men look, the way a man counting his last dollars looks at a price that's almost low enough.</p><span id="cascade-b2-beat-1"><<linkreplace "Wait.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> ...Depot. Nine sharp. You're late, you're gone. You steal, I break your hands. Now buy your own drink or get off my stool.</div></div><p>He goes back to the glass. It isn't much. But she's in — and being in, day after day, until a man like this stops noticing she's there, is the whole way the job gets done.</p>[[Leave him to it.->Location_the_anchor]]
<</linkreplace>></span></span>
<<set $game_state.current_canvas = "cap_renner_hired">>
<<set $game_state.current_node = "cap_renner_hired.base">>
<<script>>setup.markCanvasTriggered("cap_renner_hired");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_renner_hired.base") === -1) { vn.push("cap_renner_hired.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "renner_hired", "set");<</script>><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 0}]})>>
<p>He pulls out and finishes on her face, holding her there by the jaw until he's done, then letting go the way you drop a thing back in a drawer. He's already turned to the window.</p><video src="./videos/sex/mercer_finish_facial_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 1}]})>>
<p>He finishes inside her, holds, pulls out without comment. His cum slides down the inside of her thigh. He wipes himself on a cloth from the drawer and checks the time.</p><video src="./videos/sex/mercer_finish_inside_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}]})>>
<p>He finishes in her ass — and nothing comes. No give, no opening, no man-belonging-to-her. Just his weight and his cum and then his absence as he steps back. The same nothing as every time. She sets it aside with the other things she isn't allowed to ask, and straightens up.</p><video src="./videos/sex/mercer_finish_ass_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<</if>>
<<set $game_state.current_canvas = "mercer_finisher">>
<<set $game_state.current_node = "mercer_finisher.climax">>
<<script>>setup.markCanvasTriggered("mercer_finisher");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_finisher.climax") === -1) { vn.push("mercer_finisher.climax"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);setup.showEffectNotification();<</script>>
[[Straighten up.->Location_penthouse]]<p>He's already unbuckling when she crosses to him; there's never preamble. He uses her the way he uses anything he owns — fully, without thanks, his mind somewhere else the whole time.</p>
<<set $game_state.current_canvas = "mercer_serve">>
<<set $game_state.current_node = "mercer_serve.intro">>
<<script>>setup.markCanvasTriggered("mercer_serve");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_serve.intro") === -1) { vn.push("mercer_serve.intro"); }<</script>>
<<nobr>>
<<link "Down on your knees." "Canvas_mercer_serve_Node_base_knees">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/mercer_serve_knees_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>On her knees on the floor, his cock in her mouth, his hand setting the pace by her hair. He watches the city and not her, talking to no one — a maintenance note, a time he wants a thing done. She services him.</p>
<<set $game_state.current_canvas = "mercer_serve">>
<<set $game_state.current_node = "mercer_serve.base_knees">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_serve.base_knees") === -1) { vn.push("mercer_serve.base_knees"); }<</script>>
<<nobr>>
<<link "Keep sucking his cock." "Canvas_mercer_serve_Node_base_knees">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Let him bend you over the desk." "Canvas_mercer_serve_Node_base_desk">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Present your ass against the glass." "Canvas_mercer_serve_Node_base_glass">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('mercer_serve:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_mercer_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("mercer_serve:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/mercer_serve_desk_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Bent over the desk, his hand flat between her shoulder blades to keep her down, he fucks her from behind — unhurried, proprietary. Her body answers the way it always does, and he notes that it does, the way you'd note a gauge reading true.</p>
<<set $game_state.current_canvas = "mercer_serve">>
<<set $game_state.current_node = "mercer_serve.base_desk">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_serve.base_desk") === -1) { vn.push("mercer_serve.base_desk"); }<</script>>
<<nobr>>
<<link "Take it." "Canvas_mercer_serve_Node_base_desk">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back down on your knees." "Canvas_mercer_serve_Node_base_knees">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Let him take your ass." "Canvas_mercer_serve_Node_base_glass">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('mercer_serve:cc1') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_mercer_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("mercer_serve:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/mercer_serve_glass_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Against the glass wall, the lit city laid out under them, he takes her ass in long deliberate strokes, both hands on her hips. This is the act that hands her the controls of any other man alive. She braces on the glass and waits to feel it start — the give, the opening. It doesn't come. It never comes with him.</p>
<<set $game_state.current_canvas = "mercer_serve">>
<<set $game_state.current_node = "mercer_serve.base_glass">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_serve.base_glass") === -1) { vn.push("mercer_serve.base_glass"); }<</script>>
<<nobr>>
<<link "Take it in the ass." "Canvas_mercer_serve_Node_base_glass">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back over the desk." "Canvas_mercer_serve_Node_base_desk">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('mercer_serve:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass." "Canvas_mercer_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("mercer_serve:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><img src="./videos/scenes/mercer_penthouse_hub.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Mercer's penthouse. He's at the desk or the glass, and whatever he's doing he can be interrupted to use her — that's what she's for, and they both know it. He looks up when she comes in. He doesn't smile. He never smiles.</p>
<<set $game_state.current_canvas = "mercer_penthouse_hub">>
<<set $game_state.current_node = "mercer_penthouse_hub.base">>
<<script>>setup.markCanvasTriggered("mercer_penthouse_hub");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_penthouse_hub.base") === -1) { vn.push("mercer_penthouse_hub.base"); }<</script>>
<<nobr>>
<<link "Report." "Canvas_mercer_penthouse_hub_Node_report">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Kneel and serve him." "Canvas_mercer_serve_Node_intro">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Leave." "Location_atrium">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She gives him what she has — the Renner approach, the angle, the timeline. He listens the way he listens to a status light: for whether it's green.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Don't bring me progress. Bring me the man's secrets or bring me nothing. Go make yourself useful to someone who can't see what you are.</div></div>
<<set $game_state.current_canvas = "mercer_penthouse_hub">>
<<set $game_state.current_node = "mercer_penthouse_hub.report">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_penthouse_hub.report") === -1) { vn.push("mercer_penthouse_hub.report"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Go.->Location_penthouse]]<img src="./videos/scenes/mercer_penthouse.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The penthouse, by daylight. Mercer's at the glass with a tablet, and he doesn't turn when the elevator lets her in. He doesn't have to. She's company-issue, and she's here because being here is what she's for.</p><span id="cascade-b2-beat-0"><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> On your knees. I've got a call in ten.</div></div><span id="cascade-b2-beat-1"><<linkreplace "Kneel.">><p>She goes down without a word. He takes the call one-handed, her mouth around his cock, the other hand resting in her hair the way a man rests his hand on a tool he isn't using yet. He finishes before the call does. She swallows, because that's tidy, and tidy is what's expected.</p><video src="./videos/sex/mercer_first_oral_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><span id="cascade-b2-beat-2"><<linkreplace "Hear the terms.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Good. You come up when I want you, and you're like this when you do. That's the arrangement. It was always the arrangement.</div></div><p>It was. She straightens, wipes her mouth, and the shape of the days settles over her — the same shape as all the days behind it: the cradle, the call, the kneeling. World without end.</p>[[Stand up.->Location_penthouse]]
<</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "cap_first_penthouse_service">>
<<set $game_state.current_node = "cap_first_penthouse_service.base">>
<<script>>setup.markCanvasTriggered("cap_first_penthouse_service");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_first_penthouse_service.base") === -1) { vn.push("cap_first_penthouse_service.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "mercer_hub_open", "set");<</script>><p>There is nothing in the room to do. There are three things in it to notice.</p>
<<set $game_state.current_canvas = "cell_attend">>
<<set $game_state.current_node = "cell_attend.base">>
<<script>>setup.markCanvasTriggered("cell_attend");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_attend.base") === -1) { vn.push("cell_attend.base"); }<</script>>
<<nobr>>
<<link "Listen at the door." "Canvas_cell_attend_Node_listen">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Watch the man on the chair." "Canvas_cell_attend_Node_watch">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Hold still and feel the fault." "Canvas_cell_attend_Node_hold_still">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>Steel carries. She sits with her shoulder against it and counts: five voices, six, one that comes and goes and never finishes a sentence because the others stop talking when it does. Cards, some nights. A radio. A long argument about a boat that she thinks is not about a boat.</p><p>Nothing about her. That's the part she keeps turning over. Whatever she is to them, she isn't a thing they discuss.</p>
<<set $game_state.current_canvas = "cell_attend">>
<<set $game_state.current_node = "cell_attend.listen">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_attend.listen") === -1) { vn.push("cell_attend.listen"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Sit back.->Location_captive_room]]<p>There's always one, and it's always the chair. He doesn't talk to her. He reads, or he doesn't, and he watches the way a man watches a pot. When she looks back at him he holds it, bored, and eventually she is the one who looks away, and she notes that too, because a month ago she would not have.</p><p>Sometimes it's Bastien in the chair. He never reads. He sits with his hands still and studies her the way he studied her across the table, patient, waiting for the answer to a question he has not asked again.</p>
<<set $game_state.current_canvas = "cell_attend">>
<<set $game_state.current_node = "cell_attend.watch">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_attend.watch") === -1) { vn.push("cell_attend.watch"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Look away.->Location_captive_room]]<p>She lies on her back and stops doing anything at all, and listens to herself the way she listened to the door. The charge sits under her sternum with nowhere to be. It has a sound. It has, she realises, started to have a smell — hot dust, the inside of a machine that has been run too long.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "glitch_iii_seen", "operator": "is_false"}]})>>
<p>And then the fault opens, the way a badly-set bone opens, and something comes up through it that was never filed anywhere she can reach.</p><p>A hand at the back of her neck. Not holding her — resting there, the thumb moving once, slowly, along the seam. Somebody standing behind her while she looks at something she cannot see, and in the whole of her body, in the whole of the record, no fear at all. Not permitted absence of fear. The other kind. The kind that is simply not there because there is nothing in the room that could hurt her.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> Whose hand.</div></div><p>It closes. It goes back down wherever it lives. She lies in a room she cannot leave with men on the other side of the door and she finds that she is holding the back of her own neck, and that her eyes have gone wet, and that she has no idea, none, whose hand that was.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "glitch_iii_seen", "operator": "is_true"}]})>>
<p>She reaches for it again — the hand, the seam, the quiet — and it isn't there. The fault doesn't open twice for the same thing. What's left is the pressure, and the hot-dust smell, and the certainty that it is worse than it was.</p>
<</if>>
<<set $game_state.current_canvas = "cell_attend">>
<<set $game_state.current_node = "cell_attend.hold_still">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_attend.hold_still") === -1) { vn.push("cell_attend.hold_still"); }<</script>>
<<script>>advanceTime(30);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "glitch_iii_seen", "set");<</script>>
[[Let it go.->Location_captive_room]]<p>The light doesn't go off, so she turns her face to the wall and powers down anyway. The bed is bolted down and the mattress smells of other people. She goes under fast, the way she always does, and it doesn't matter at all, because the door opens in the night. It always opens in the night.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "trait", "subject": "player", "trait_key": "core_strain", "operator": "lt", "value": 24}]})>>
<video src="./videos/sex/cell_sleep_nominal.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She's down on the mat with her back to the wall, trying to drop under, when the weight settles in behind her and Bastien starts using her before she's all the way awake — cock worked into her cunt from behind, unhurried, one hand laid flat on the sternum plate so he can feel it tick while he does it. The charge climbs with every slow shove and runs down to the empty socket at the base of her spine and stops there, nowhere to go, stacking behind the plate in the dark.</p><p>He finishes inside her and the read-out clicks up one notch and holds — it never drops — and he wipes off on the small of her back and leaves her where she lies. <em>Logged as rest. It isn't.</em></p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Don't mind me. Go back under, filthy thing — I'm just reading you.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> There it goes. One more on the count and nowhere for it to run. Bought that too.</div></div>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "core_strain", "operator": "gte", "value": 24}, {"type": "trait", "subject": "player", "trait_key": "core_strain", "operator": "lt", "value": 48}]})>>
<video src="./videos/sex/cell_sleep_hot.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She surfaces because there are hands on her that aren't one man's — the crew have let themselves in in the dark, and she wakes already being used, a cock working her cunt while another feeds itself into her mouth and a third pair of hands just paws her tits like they're checking she's still warm. She can't tell how many; they pass her between them in the black, laughing low, and every load that goes in stacks up behind the plate with the drain gone and the socket empty, filling and filling with nowhere to send it.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Left the door open for the lads. She sleeps better with a hole full — don't you, slut.</div></div><p>"She's awake — watch, the little machine's awake."</p><p>"Doesn't matter if she is. Open, bought-and-paid-for."</p><p>"Pass her down, I've not had a turn."</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "core_strain", "operator": "gte", "value": 48}, {"type": "trait", "subject": "player", "trait_key": "core_strain", "operator": "lt", "value": 72}]})>>
<video src="./videos/sex/cell_sleep_faulting.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>They don't let her stay under at all now — every time she starts to slip they haul her back onto her knees and use her hard, one fisting her hair down onto his cock till she gags and drools, another driving into her from behind, handprints coming up on her ass and a set of fingermarks banding her throat. Then they flip her and take her in the ass — the drain inverted, the thing that used to TAKE from a man now only pouring INTO her — and Bastien holds her jaw and makes her come on it because forcing it spikes the read-out fastest, and the number jumps two, three, climbing in the dark while she can't tell sleep from being fucked.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Make her come — that's what moves the count, not the fucking. Force it. Watch the plate, not her.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Up it goes. Look at that. Passed round all night and still filling.</div></div><p>"She's shaking — do it again, it likes it."</p><p>"Hold her mouth, she's trying to check out."</p><p>"Mark her up, no one's counting bruises."</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "trait", "subject": "player", "trait_key": "core_strain", "operator": "gte", "value": 72}]})>>
<video src="./videos/sex/cell_sleep_failing.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>By the small hours her body's half-quitting under them — legs gone loose, breath sawing, the sternum plate flickering its read-out where it should hold steady — and none of them slow down for it. The charge has stacked up so high with nowhere to drain that every load now lands as a wet flare of pain instead of anything else, and they force her over the edge anyway, again, the pleasure coming out the wrong way and hurting, the plate stuttering the number up past where the light stays on. She's limp between them, used front and back, drooled-on and slack, and Bastien just leans in close to read the flicker off her chest and not her face at all.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> It's browning out. Good — keep going. I want to see how high it climbs before the light won't hold.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Bastien:</strong> Not her, idiot — the plate. Watch the plate. She's just the socket.</div></div><p>"Something's wrong with it — look, it's flickering."</p><p>"Boss says keep going, so keep going."</p><p>"Still warm, still open. Who cares if it's breaking."</p>
<</if>>
<<set $game_state.current_canvas = "cell_sleep">>
<<set $game_state.current_node = "cell_sleep.base">>
<<script>>setup.markCanvasTriggered("cell_sleep");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_sleep.base") === -1) { vn.push("cell_sleep.base"); }<</script>>
<<script>>advanceTime(540);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "energy", "add", 100.0, true, null);setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.showEffectNotification();<</script>>
[[Wake up.->Location_captive_room]]<p>She strips and stands under the water until whatever the work left on her is gone — sweat, a client's hands, the smell of the docks. The company keeps her maintained; this part she does for herself, the one washing that isn't on anyone's schedule. She comes out clean, and for a minute, almost new.</p>
<<set $game_state.current_canvas = "activity_wash">>
<<set $game_state.current_node = "activity_wash.base">>
<<script>>setup.markCanvasTriggered("activity_wash");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_wash.base") === -1) { vn.push("activity_wash.base"); }<</script>>
<<script>>advanceTime(30);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "hygiene", "add", 100.0, true, null);setup.showEffectNotification();<</script>>
[[Dry off. Dressed.->Location_wren_room]]<img src="./videos/locations/underworld_market.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Coin moves here for everything the Spire pretends doesn't exist. Blades and field-gear on the near tables; further back, behind chain, the part she doesn't look at directly. She has coin now. She can leave with an edge.</p>
<<set $game_state.current_canvas = "underworld_market_shop">>
<<set $game_state.current_node = "underworld_market_shop.base">>
<<script>>setup.markCanvasTriggered("underworld_market_shop");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_market_shop.base") === -1) { vn.push("underworld_market_shop.base"); }<</script>>
<<nobr>>
<<if setup.checkCostsAffordable([{"trait": "coin", "value": 40}])>>
<<link "Buy a weapon — something with an edge." "Canvas_underworld_market_shop_Node_weapon">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "fighting", "add", 5.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", -40, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<else>>
<span class="locked-choice" title="Buy a weapon — something with an edge.">Buy a weapon — something with an edge. (<<= setup.getCostBlockedMessage([{"trait": "coin", "value": 40}])>>)</span><br>
<</if>>
<<if setup.checkCostsAffordable([{"trait": "coin", "value": 30}])>>
<<link "Buy infiltration gear." "Canvas_underworld_market_shop_Node_gear">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "stealth", "add", 5.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", -30, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<else>>
<span class="locked-choice" title="Buy infiltration gear.">Buy infiltration gear. (<<= setup.getCostBlockedMessage([{"trait": "coin", "value": 30}])>>)</span><br>
<</if>>
<<link "Walk the pens." "Canvas_underworld_market_shop_Node_pens">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Leave the stalls." "Location_underworld_market">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She buys a blade the way a soldier does — by balance, not by looks — and a thing to hide it in. Her hands are deadlier leaving than they were coming in. Coin well spent.</p>
<<set $game_state.current_canvas = "underworld_market_shop">>
<<set $game_state.current_node = "underworld_market_shop.weapon">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_market_shop.weapon") === -1) { vn.push("underworld_market_shop.weapon"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Stow it.->Location_underworld_market]]<p>Soft soles, a light that isn't a light, a tool roll that opens what's meant to stay shut. The kind of kit that turns a careful woman into one nobody sees. Coin well spent.</p>
<<set $game_state.current_canvas = "underworld_market_shop">>
<<set $game_state.current_node = "underworld_market_shop.gear">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_market_shop.gear") === -1) { vn.push("underworld_market_shop.gear"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Pack it away.->Location_underworld_market]]<p>Behind the chain they keep people the way the market keeps anything — by weight and condition, tagged, waiting on a buyer. She walks the line because she makes herself walk it. Most are just people, stolen and sold. But one of them, a girl too still, too even, watches her back with eyes that don't track quite right — and something in Wren's chest does a thing it has no name for, a recognition she shuts off before it finishes. She's looked at her own reflection in a cage. She shuts it down and walks out.</p>
<<set $game_state.current_canvas = "underworld_market_shop">>
<<set $game_state.current_node = "underworld_market_shop.pens">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_market_shop.pens") === -1) { vn.push("underworld_market_shop.pens"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Don't look again.->Location_underworld_market]]<p>The ring is a pit of packed dirt under a single bulb, the crowd three deep around it laying coin on the next body to fall. The matchmaker looks her up and down, unimpressed, and asks what she thinks she can take. Down here her fists pay the same as the rest of her.</p>
<<set $game_state.current_canvas = "underworld_pit_fight">>
<<set $game_state.current_node = "underworld_pit_fight.base">>
<<script>>setup.markCanvasTriggered("underworld_pit_fight");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_pit_fight.base") === -1) { vn.push("underworld_pit_fight.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 10}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('underworld_pit_fight:cc0') ? '' : 'unlocked-choice'">
<<link "Take a low bout." "Canvas_underworld_pit_fight_Node_win_low">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", 8.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("underworld_pit_fight:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Take a low bout.">Take a low bout. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 30}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 20}])>>
<span @class="setup.isChoiceVisited('underworld_pit_fight:cc1') ? '' : 'unlocked-choice'">
<<link "Take a hard bout." "Canvas_underworld_pit_fight_Node_win_hard">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", 20.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -20, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("underworld_pit_fight:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Take a hard bout.">Take a hard bout. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 20}])>>)</span><br>
<</if>>
<</if>>
<<link "Just watch tonight." "Location_underworld_pit">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>They put a brawler in with her, slow and heavy. She lets him swing, reads the half-beat, and ends it with the kind of economy the crowd doesn't cheer because it scared them. The matchmaker pays out without meeting her eyes.</p>
<<set $game_state.current_canvas = "underworld_pit_fight">>
<<set $game_state.current_node = "underworld_pit_fight.win_low">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_pit_fight.win_low") === -1) { vn.push("underworld_pit_fight.win_low"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Take the purse.->Location_underworld_pit]]<p>This one's real — fast, trained, in it to hurt her. For a few seconds it's almost a fight. Then the thing they built her to be takes over, and what's left on the dirt isn't getting up. The crowd goes quiet. The purse is heavy. Someone in the dark is watching her now.</p>
<<set $game_state.current_canvas = "underworld_pit_fight">>
<<set $game_state.current_node = "underworld_pit_fight.win_hard">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_pit_fight.win_hard") === -1) { vn.push("underworld_pit_fight.win_hard"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Take the purse.->Location_underworld_pit]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 0}]})>>
<p>He holds her head still and finishes in her mouth; she takes it, swallows because the trade expects it, and sits back on her heels. He's already reaching for his drink.</p><video src="./videos/sex/brothel_finish_facial_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 1}]})>>
<p>He finishes inside her with a grunt, holds, then pulls out. His cum slides down the inside of her thigh. She waits the polite beat and lifts off, and counts the coin into her palm.</p><video src="./videos/sex/brothel_finish_inside_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}]})>>
<p>He finishes in her ass and stays a moment, breathing, then steps back. His weight and his cum and then nothing — the same nothing as every man down here. She straightens. The next one's already on the stairs.</p><video src="./videos/sex/brothel_finish_ass_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<</if>>
<<set $game_state.current_canvas = "underworld_brothel_finisher">>
<<set $game_state.current_node = "underworld_brothel_finisher.climax">>
<<script>>setup.markCanvasTriggered("underworld_brothel_finisher");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_brothel_finisher.climax") === -1) { vn.push("underworld_brothel_finisher.climax"); }<</script>>
<<script>>advanceTime(120);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "coin", "add", 10.0, false, null);setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);setup.showEffectNotification();<</script>>
[[Clean up. Collect.->Location_underworld_brothel]]<p>The madam points her at a door. The man inside doesn't look up from his drink until she's undressed — then he looks her over the way men look over rented things, deciding where to start. No name, no preamble. He paid for the hour and means to use it.</p>
<<set $game_state.current_canvas = "underworld_brothel_loop">>
<<set $game_state.current_node = "underworld_brothel_loop.intro">>
<<script>>setup.markCanvasTriggered("underworld_brothel_loop");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_brothel_loop.intro") === -1) { vn.push("underworld_brothel_loop.intro"); }<</script>>
<<nobr>>
<<link "Down on your knees." "Canvas_underworld_brothel_loop_Node_base_oral">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/brothel_oral_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>On her knees between his, she takes his cock in her mouth and works it the way the trade taught her — steady, practiced, her eyes somewhere past his hip. He fists a hand in her hair and sets the pace. She lets him. None of it reaches her.</p>
<<set $game_state.current_canvas = "underworld_brothel_loop">>
<<set $game_state.current_node = "underworld_brothel_loop.base_oral">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_brothel_loop.base_oral") === -1) { vn.push("underworld_brothel_loop.base_oral"); }<</script>>
<<nobr>>
<<link "Keep sucking him." "Canvas_underworld_brothel_loop_Node_base_oral">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Climb on and ride him." "Canvas_underworld_brothel_loop_Node_base_vaginal">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Turn around and give him your ass." "Canvas_underworld_brothel_loop_Node_base_anal">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('underworld_brothel_loop:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_underworld_brothel_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("underworld_brothel_loop:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/brothel_client.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She climbs on and takes his cock without ceremony, sinks down and rides, hips keeping a rhythm that has nothing to do with her. He paws at her tits and fucks up into her; she gives him the small sounds the trade expects and feels none of them.</p>
<<set $game_state.current_canvas = "underworld_brothel_loop">>
<<set $game_state.current_node = "underworld_brothel_loop.base_vaginal">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_brothel_loop.base_vaginal") === -1) { vn.push("underworld_brothel_loop.base_vaginal"); }<</script>>
<<nobr>>
<<link "Keep riding him." "Canvas_underworld_brothel_loop_Node_base_vaginal">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back down on your knees." "Canvas_underworld_brothel_loop_Node_base_oral">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Let him have your ass." "Canvas_underworld_brothel_loop_Node_base_anal">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('underworld_brothel_loop:cc1') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_underworld_brothel_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("underworld_brothel_loop:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/brothel_anal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Bent over the edge of the bed, she takes him in the ass in long flat strokes, both his hands clamped on her hips. Just more friction down a body that gives nothing back. She braces and waits it out, counting somewhere behind her own face what the hour's worth.</p>
<<set $game_state.current_canvas = "underworld_brothel_loop">>
<<set $game_state.current_node = "underworld_brothel_loop.base_anal">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_brothel_loop.base_anal") === -1) { vn.push("underworld_brothel_loop.base_anal"); }<</script>>
<<nobr>>
<<link "Take it." "Canvas_underworld_brothel_loop_Node_base_anal">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back onto his cock." "Canvas_underworld_brothel_loop_Node_base_vaginal">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('underworld_brothel_loop:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass." "Canvas_underworld_brothel_finisher_Node_climax">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("underworld_brothel_loop:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><p>The madam counts her with one look and tips her head at the rooms. No contract, no company, no Mercer signing her out — just a door, a client, and coin at the end that goes in her pocket and nowhere else. The thing she's built for, sold for once, on her own account.</p>
<<set $game_state.current_canvas = "underworld_brothel_work">>
<<set $game_state.current_node = "underworld_brothel_work.base">>
<<script>>setup.markCanvasTriggered("underworld_brothel_work");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_brothel_work.base") === -1) { vn.push("underworld_brothel_work.base"); }<</script>>
<<nobr>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<<link "Take a client." "Canvas_underworld_brothel_loop_Node_intro">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<else>>
<span class="locked-choice" title="Take a client.">Take a client. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<<link "Not tonight." "Location_underworld_brothel">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/locations/underworld_strip.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She walks back up the strip — the house, the pit, the market closing behind her, and past them the dark where the real city keeps its teeth. Down here nobody was looking for her. It's the closest thing to free she's felt.</p>
<<set $game_state.current_canvas = "underworld_strip_hub">>
<<set $game_state.current_node = "underworld_strip_hub.base">>
<<script>>setup.markCanvasTriggered("underworld_strip_hub");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_strip_hub.base") === -1) { vn.push("underworld_strip_hub.base"); }<</script>>
<<script>>advanceTime(30);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "energy", "add", -10.0, true, null);setup.showEffectNotification();<</script>>
[[Out to the gate.->Location_underworld_gate]]<img src="./videos/locations/underworld_gate.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The man at the door doesn't ask her name. He looks her over the way a turnstile would and holds out a hand. "Coin," he says. "Or you're not my problem." Behind him the stairs go down into red light and noise. There's more than one way through a man like this. The coin he wants isn't company credits — it's underworld scrip: earned down here, spent down here, worth nothing up top.</p>
<<set $game_state.current_canvas = "underworld_gate_check">>
<<set $game_state.current_node = "underworld_gate_check.base">>
<<script>>setup.markCanvasTriggered("underworld_gate_check");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_gate_check.base") === -1) { vn.push("underworld_gate_check.base"); }<</script>>
<<nobr>>
<<if setup.checkCostsAffordable([{"trait": "coin", "value": 5}])>>
<<link "Pay the toll." "Location_underworld_strip">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", -5, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<else>>
<span class="locked-choice" title="Pay the toll.">Pay the toll. (<<= setup.getCostBlockedMessage([{"trait": "coin", "value": 5}])>>)</span><br>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 15}]})>>
<span @class="setup.isChoiceVisited('underworld_gate_check:cc0') ? '' : 'unlocked-choice'">
<<link "Force past him." "Location_underworld_strip">><<script>>setup.markChoiceVisited("underworld_gate_check:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "emitter_broken", "operator": "lt", "value": 1}]})>>
<<if setup.checkCostsAffordable([{"trait": "arousal_charge", "value": 1}])>>
<span @class="setup.isChoiceVisited('underworld_gate_check:cc1') ? '' : 'unlocked-choice'">
<<link "Put the weapon on him." "Canvas_underworld_gate_check_Node_weapon_scene">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "arousal_charge", "add", -1, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("underworld_gate_check:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Put the weapon on him.">Put the weapon on him. (<<= setup.getCostBlockedMessage([{"trait": "arousal_charge", "value": 1}])>>)</span><br>
<</if>>
<</if>>
<<link "Turn back." "Location_the_waterfront">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/arousal_weapon_use.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She thumbs the emitter and the doorman's whole face changes — the bored menace gone, breath short, the hand that wanted coin wanting something else now. She walks him back into the shadow of the stair, gets his cock out, and rides him against the wall in quick flat strokes, fast and clinical, giving him nothing but the friction. He spills into her with his eyes rolling white and sags down the bricks, under. He'll wake on the steps in an hour with no memory, and never know she walked past him. The stairs are open.</p>
<<set $game_state.current_canvas = "underworld_gate_check">>
<<set $game_state.current_node = "underworld_gate_check.weapon_scene">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_gate_check.weapon_scene") === -1) { vn.push("underworld_gate_check.weapon_scene"); }<</script>>
<<script>>advanceTime(60);<</script>>
[[Down into the strip.->Location_underworld_strip]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}]})>>
<p>The emitter sits charged on the bench. One core, one weapon — it feeds the drain inside her or the thing in her hand, never both at once. Right now it's the drain.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]})>>
<p>The emitter's live in her hand, the drain inside her gone quiet behind it. One core, one weapon. Right now it's the emitter.</p>
<</if>>
<<set $game_state.current_canvas = "activity_swap_weapon">>
<<set $game_state.current_node = "activity_swap_weapon.base">>
<<script>>setup.markCanvasTriggered("activity_swap_weapon");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_swap_weapon.base") === -1) { vn.push("activity_swap_weapon.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}]})>>
<span @class="setup.isChoiceVisited('activity_swap_weapon:cc0') ? '' : 'unlocked-choice'">
<<link "Run the emitter instead." "Location_wren_room">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("activity_swap_weapon:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]})>>
<span @class="setup.isChoiceVisited('activity_swap_weapon:cc1') ? '' : 'unlocked-choice'">
<<link "Go back to the drain." "Location_wren_room">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("activity_swap_weapon:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<link "Leave it." "Location_wren_room">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The emitter sits on her bench, casing off, guts exposed. She knows the architecture of it the way she knows her own — because it is her own, scaled down to a handheld. Heat-warp on the field coils, a cracked driver. Slow work, and hers alone.</p>
<<set $game_state.current_canvas = "activity_repair_weapon">>
<<set $game_state.current_node = "activity_repair_weapon.base">>
<<script>>setup.markCanvasTriggered("activity_repair_weapon");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_repair_weapon.base") === -1) { vn.push("activity_repair_weapon.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "weapon_repair", "operator": "lt", "value": 2}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('activity_repair_weapon:cc0') ? '' : 'unlocked-choice'">
<<link "Work on it." "Canvas_activity_repair_weapon_Node_progress">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "weapon_repair", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("activity_repair_weapon:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Work on it.">Work on it. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "weapon_repair", "operator": "gte", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "weapon_repair", "operator": "lt", "value": 3}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('activity_repair_weapon:cc1') ? '' : 'unlocked-choice'">
<<link "Finish it." "Canvas_activity_repair_weapon_Node_done">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "weapon_repair", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "arousal_charge", "set", 3.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "arousal_weapon_ready", "set");<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("activity_repair_weapon:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Finish it.">Finish it. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<link "Leave it for now." "Location_wren_room">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "weapon_repair", "operator": "lt", "value": 2}]})>>
<p>She straightens the field coils, one pass, and tests the draw. Dead still — but cleaner dead than it was. More to do.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "weapon_repair", "operator": "gte", "value": 2}]})>>
<p>Driver reseated, coils true. It draws a charge now and holds it — a flicker of the field crossing her own skin, the faint unwanted warmth of it. Almost. One more pass and it fires.</p>
<</if>>
<<set $game_state.current_canvas = "activity_repair_weapon">>
<<set $game_state.current_node = "activity_repair_weapon.progress">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_repair_weapon.progress") === -1) { vn.push("activity_repair_weapon.progress"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Set it down.->Location_wren_room]]<p>Last pass. She powers it and the field comes up clean, a low hum she feels in her teeth — and, testing it on herself a half-second too long, a heat low in her belly she didn't ask for and shuts off fast. It works. A second weapon now, hers, that the company never issued and doesn't know she has. A way past any man she can't fight.</p>
<<set $game_state.current_canvas = "activity_repair_weapon">>
<<set $game_state.current_node = "activity_repair_weapon.done">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_repair_weapon.done") === -1) { vn.push("activity_repair_weapon.done"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Stow it.->Location_wren_room]]<p>The strongroom is what he saved. Two personnel files, a man each — his own people, the two who moved the materials. Both stamped CLOSED. Under each, a death notice, and under that, more notices: wives, children, the whole line, the dates all the same night. Renner didn't just fire them. He erased them. And paperclipped to the back, the thing he's really hiding: where the two men came from before Vance — the same name twice, a place down in the city that isn't on any map. The underworld. That's the thread Cain followed back. That's where it goes.</p><p>Standing in it, something turns over in her — not a thought, a feeling, brief and uninvited: she has stood in a room like this before, on the other side of the file. Then it's gone, filed under nothing.</p>
<<set $game_state.current_canvas = "yard_find_3">>
<<set $game_state.current_node = "yard_find_3.base">>
<<script>>setup.markCanvasTriggered("yard_find_3");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_find_3.base") === -1) { vn.push("yard_find_3.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "found_dead_men", "set");<</script>>
[[Take what matters. Go.->Location_renner_burned_yard]]<img src="./videos/activities/arousal_weapon.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>In a melted equipment locker, half-slagged but whole at the core, is a device she knows in her hands before she knows in her head — the same build as the thing inside her, the one that makes a body want against its own sense. A field emitter. Renner had one off the line and never fired it. Heat-warped, dead. But the core is sound. She pockets it. Fixed, it's a way past any man who'd otherwise put her down.</p>
<<set $game_state.current_canvas = "yard_find_2">>
<<set $game_state.current_node = "yard_find_2.base">>
<<script>>setup.markCanvasTriggered("yard_find_2");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_find_2.base") === -1) { vn.push("yard_find_2.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "has_arousal_weapon", "set");<</script>>
[[Take it. Get out clean.->Location_renner_burned_yard]]<p>Inside the outer line she finds the shape of it under the burn: not just a storage yard. Loading bays built for things heavier than crates, restraint points set into a slab, a billing wall scorched but legible — shipments logged out to the same Vance facility she's already walked the ashes of. Renner moved more than equipment through here. He just never asked what for.</p>
<<set $game_state.current_canvas = "yard_find_1">>
<<set $game_state.current_node = "yard_find_1.base">>
<<script>>setup.markCanvasTriggered("yard_find_1");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_find_1.base") === -1) { vn.push("yard_find_1.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "yard_clue_seen", "set");<</script>>
[[Log it. Keep moving.->Location_renner_burned_yard]]<img src="./videos/locations/renner_burned_yard.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 1}]})>>
<p>The fence is down on the water side. Past it the yard is black glass and bent steel — and a guard with a torch, walking the line where the gate used to be. Getting in means getting past him.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 3}]})>>
<p>She's inside the outer line now. Deeper in there are more lights and more men — Renner doesn't watch a dead yard this hard for nothing. Whatever's worth guarding is further back.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "gte", "value": 3}]})>>
<p>She's been all the way to the back of it. There's nothing left in the yard she hasn't already put her hands on.</p>
<</if>>
<<set $game_state.current_canvas = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.base">>
<<script>>setup.markCanvasTriggered("yard_crawl");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.base") === -1) { vn.push("yard_crawl.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 1}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('yard_crawl:cc0') ? '' : 'unlocked-choice'">
<<link "Push in past the gate." "Canvas_yard_crawl_Node_guard1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Push in past the gate.">Push in past the gate. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 2}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('yard_crawl:cc1') ? '' : 'unlocked-choice'">
<<link "Work deeper in." "Canvas_yard_crawl_Node_guard2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Work deeper in.">Work deeper in. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "gte", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 3}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('yard_crawl:cc2') ? '' : 'unlocked-choice'">
<<link "Push to the back of it." "Canvas_yard_crawl_Node_guard3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Push to the back of it.">Push to the back of it. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<link "Slip back out." "Location_the_waterfront">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>One man, one torch, bored and slow — the easy one, the test before the real ones. He hasn't seen her yet.</p>
<<set $game_state.current_canvas = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.guard1">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.guard1") === -1) { vn.push("yard_crawl.guard1"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 10}]})>>
<span @class="setup.isChoiceVisited('yard_crawl:cc3') ? '' : 'unlocked-choice'">
<<link "Slip past him, unseen." "Canvas_yard_crawl_Node_deeper">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc3");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 10}]})>>
<span @class="setup.isChoiceVisited('yard_crawl:cc4') ? '' : 'unlocked-choice'">
<<link "Take him down." "Canvas_yard_crawl_Node_deeper">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc4");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "emitter_broken", "operator": "lt", "value": 1}]})>>
<span @class="setup.isChoiceVisited('yard_crawl:cc5') ? '' : 'unlocked-choice'">
<<link "Use the weapon on him." "Canvas_yard_crawl_Node_weapon_scene">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "arousal_charge", "add", -1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc5");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}]})>>
<span @class="setup.isChoiceVisited('yard_crawl:cc6') ? '' : 'unlocked-choice'">
<<link "He's on you — break and run." "Canvas_yard_crawl_Node_flee">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc6");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 10}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 10}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "emitter_broken", "operator": "lt", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}]})))>>
<<script>>
try {
window._engineNoExitsDiag_yard_crawl = [{"text": "Slip past him, unseen.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 10}]}}, {"text": "Take him down.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 10}]}}, {"text": "Use the weapon on him.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "emitter_broken", "operator": "lt", "value": 1}]}}, {"text": "He's on you \u2014 break and run.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "yard_crawl",
choices: window._engineNoExitsDiag_yard_crawl,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_yard_crawl>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas yard_crawl</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Location_renner_burned_yard]]
<</if>>
<</nobr>><p>Two of them here, awake and armed, working a pattern between the racks. Past the easy line — these ones would hurt her if she let them.</p>
<<set $game_state.current_canvas = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.guard2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.guard2") === -1) { vn.push("yard_crawl.guard2"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 25}]})>>
<span @class="setup.isChoiceVisited('yard_crawl:cc7') ? '' : 'unlocked-choice'">
<<link "Slip past them, unseen." "Canvas_yard_crawl_Node_deeper">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc7");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 25}]})>>
<span @class="setup.isChoiceVisited('yard_crawl:cc8') ? '' : 'unlocked-choice'">
<<link "Take them down." "Canvas_yard_crawl_Node_deeper">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc8");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "emitter_broken", "operator": "lt", "value": 1}]})>>
<span @class="setup.isChoiceVisited('yard_crawl:cc9') ? '' : 'unlocked-choice'">
<<link "Use the weapon on them." "Canvas_yard_crawl_Node_weapon_scene">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "arousal_charge", "add", -1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc9");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}]})>>
<span @class="setup.isChoiceVisited('yard_crawl:cc10') ? '' : 'unlocked-choice'">
<<link "Too many — break and run." "Canvas_yard_crawl_Node_flee">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc10");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 25}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 25}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "emitter_broken", "operator": "lt", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}]})))>>
<<script>>
try {
window._engineNoExitsDiag_yard_crawl = [{"text": "Slip past them, unseen.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 25}]}}, {"text": "Take them down.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 25}]}}, {"text": "Use the weapon on them.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "emitter_broken", "operator": "lt", "value": 1}]}}, {"text": "Too many \u2014 break and run.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "yard_crawl",
choices: window._engineNoExitsDiag_yard_crawl,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_yard_crawl>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas yard_crawl</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Location_renner_burned_yard]]
<</if>>
<</nobr>><p>The heaviest watch is here, at the back — a knot of his best men around a strongroom the fire didn't reach. Whatever Renner's keeping, it's behind them.</p>
<<set $game_state.current_canvas = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.guard3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.guard3") === -1) { vn.push("yard_crawl.guard3"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 40}]})>>
<span @class="setup.isChoiceVisited('yard_crawl:cc11') ? '' : 'unlocked-choice'">
<<link "Ghost straight through them." "Canvas_yard_crawl_Node_deeper">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc11");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 40}]})>>
<span @class="setup.isChoiceVisited('yard_crawl:cc12') ? '' : 'unlocked-choice'">
<<link "Cut through them." "Canvas_yard_crawl_Node_deeper">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc12");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "emitter_broken", "operator": "lt", "value": 1}]})>>
<span @class="setup.isChoiceVisited('yard_crawl:cc13') ? '' : 'unlocked-choice'">
<<link "Use the weapon on them." "Canvas_yard_crawl_Node_weapon_scene">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "arousal_charge", "add", -1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc13");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}]})>>
<span @class="setup.isChoiceVisited('yard_crawl:cc14') ? '' : 'unlocked-choice'">
<<link "Not yet — break and run." "Canvas_yard_crawl_Node_flee">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl:cc14");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 40}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 40}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "emitter_broken", "operator": "lt", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}]})))>>
<<script>>
try {
window._engineNoExitsDiag_yard_crawl = [{"text": "Ghost straight through them.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 40}]}}, {"text": "Cut through them.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 40}]}}, {"text": "Use the weapon on them.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "emitter_broken", "operator": "lt", "value": 1}]}}, {"text": "Not yet \u2014 break and run.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "yard_crawl",
choices: window._engineNoExitsDiag_yard_crawl,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_yard_crawl>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas yard_crawl</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Location_renner_burned_yard]]
<</if>>
<</nobr>><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 2}]})>>
<p>She's through the gate and into the outer yard, the guard behind her none the wiser or none the able. The dark swallows her.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "gte", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 3}]})>>
<p>Deeper now, in among the racks where the lights are — and past the men who'd have stopped her. She keeps to the shadow and goes on.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "gte", "value": 3}]})>>
<p>The back of the yard, the strongroom, the thing they were all standing around. She's in.</p>
<</if>>
<<set $game_state.current_canvas = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.deeper">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.deeper") === -1) { vn.push("yard_crawl.deeper"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[See what's here.->Location_renner_burned_yard]]<video src="./videos/sex/yard_emitter_fire_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She thumbs the emitter and the field rolls out ahead of her. Whoever's in reach goes stupid with it inside a breath — pupils blown, breath ragged, the gun forgotten in a slack hand. She picks the nearest, drags his cock out of his fatigues, and sinks down onto it against the crates, riding him in hard flat strokes while the others sway on their feet. There's nothing in it on her side — just leverage; she works him until he spills into her and drops boneless and out. The rest fold a beat later. By the time any of them surface she's gone, deeper in, his cum drying on her thigh and nothing in her face at all.</p>
<<set $game_state.current_canvas = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.weapon_scene">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.weapon_scene") === -1) { vn.push("yard_crawl.weapon_scene"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Step over them. On.->Location_renner_burned_yard]]<p>It goes against her — too many, or too sharp, or her own body too slow. She breaks for the downed fence and is over the water side before the torches swing round. Nothing gained tonight. She walks it off.</p>
<<set $game_state.current_canvas = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.flee">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.flee") === -1) { vn.push("yard_crawl.flee"); }<</script>>
<<script>>advanceTime(60);<</script>>
[[Get clear.->Location_the_waterfront]]<img src="./videos/locations/facility_ruins.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The facility is a black skeleton against the dock lights — scorched frame, melted racks, the smell of old smoke that never washed out. She walks it slowly, cataloguing: where the containment was, where the fire started, what the dead machines used to do. None of it matches the briefing. All of it was built to hold things that could feel — and someone came through and let them out before he burned it down. The dread-place. The first one.</p>
<<set $game_state.current_canvas = "inv_facility_explore">>
<<set $game_state.current_node = "inv_facility_explore.base">>
<<script>>setup.markCanvasTriggered("inv_facility_explore");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("inv_facility_explore.base") === -1) { vn.push("inv_facility_explore.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Done here.->Location_facility_ruins]]<p>The floor's empty, the door shut. No one watches an asset in its own quarters. She uses the hour the way the company never told her to — drilling the body they built, sharpening it past the spec on her file. It's the one thing here that's only hers.</p>
<<set $game_state.current_canvas = "activity_train">>
<<set $game_state.current_node = "activity_train.base">>
<<script>>setup.markCanvasTriggered("activity_train");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_train.base") === -1) { vn.push("activity_train.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "lt", "value": 30}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('activity_train:cc0') ? '' : 'unlocked-choice'">
<<link "Combat drill." "Canvas_activity_train_Node_combat">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "fighting", "add", 3.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("activity_train:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Combat drill.">Combat drill. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 30}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "lt", "value": 50}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('activity_train:cc1') ? '' : 'unlocked-choice'">
<<link "Combat drill." "Canvas_activity_train_Node_combat">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "fighting", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("activity_train:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Combat drill.">Combat drill. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "yard_caught_once", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 30}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('activity_train:cc2') ? '' : 'unlocked-choice'">
<<link "Stealth drill." "Canvas_activity_train_Node_stealth">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "stealth", "add", 3.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("activity_train:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Stealth drill.">Stealth drill. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "yard_caught_once", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 30}, {"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 50}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('activity_train:cc3') ? '' : 'unlocked-choice'">
<<link "Stealth drill." "Canvas_activity_train_Node_stealth">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "stealth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("activity_train:cc3");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Stealth drill.">Stealth drill. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<link "Enough for now." "Location_wren_room">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "lt", "value": 25}]})>>
<p>She runs the forms against the empty air — strikes, breaks, the throw that ends a man. Her body obeys a half-beat slow, the way a tool obeys before it's been worked hard. Clumsy still. But the half-beat is shorter than it was yesterday.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 25}]})>>
<p>No hesitation now. The forms come fast and flat and final — strike, break, the throw — each one landing where a throat or a knee would be. Whatever they built her for, this wasn't on the spec. She's better than the file says. She keeps that to herself.</p>
<</if>>
<<set $game_state.current_canvas = "activity_train">>
<<set $game_state.current_node = "activity_train.combat">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_train.combat") === -1) { vn.push("activity_train.combat"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Rack it. Catch your breath.->Location_wren_room]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}]})>>
<p>She practices the small things — weight rolled onto the outside of the foot, breath dropped low, the timing of a turned back. Her own joints betray her with a tick of sound she'd never have caught a week ago. She catches it now. That's the start of it.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 25}]})>>
<p>She moves through her own quarters and the room doesn't register her — no sound, no shift of air, nothing for a camera to hold. A thing that can't be seen can go where it isn't allowed. She doesn't let the thought finish.</p>
<</if>>
<<set $game_state.current_canvas = "activity_train">>
<<set $game_state.current_node = "activity_train.stealth">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_train.stealth") === -1) { vn.push("activity_train.stealth"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Ease off. Settle.->Location_wren_room]]<img src="./videos/activities/chip_view.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She opens the store the company gave her for what it calls recovered assets — the place a restored memory would go, if she ever recovered one. It's empty. It has always been empty.</p><p>There is one slot in it, greyed out, labelled in a hand that isn't the company's: FRAGMENT 01 — LOCKED. She doesn't remember making the label. She closes the store — and notices she's opened it more often than the task log would ever justify. She closes that thought too.</p>
<<set $game_state.current_canvas = "canvas_chip_view">>
<<set $game_state.current_node = "canvas_chip_view.base">>
<<script>>setup.markCanvasTriggered("canvas_chip_view");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_chip_view.base") === -1) { vn.push("canvas_chip_view.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Close it.->Location_cradle]]<p>She bleeds the cradle's charge into the emitter's cell until it reads full — three. Then she pockets it and goes.</p>
<<set $game_state.current_canvas = "activity_recharge_emitter">>
<<set $game_state.current_node = "activity_recharge_emitter.base">>
<<script>>setup.markCanvasTriggered("activity_recharge_emitter");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_recharge_emitter.base") === -1) { vn.push("activity_recharge_emitter.base"); }<</script>>
<<script>>advanceTime(60);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "arousal_charge", "set", 3.0, false, null);setup.showEffectNotification();<</script>>
[[Done.->Location_cradle]]<p>A few minutes on the feed line and the drain rebuilds its one shot. Loaded. She unhooks.</p>
<<set $game_state.current_canvas = "activity_recharge_drain">>
<<set $game_state.current_node = "activity_recharge_drain.base">>
<<script>>setup.markCanvasTriggered("activity_recharge_drain");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_recharge_drain.base") === -1) { vn.push("activity_recharge_drain.base"); }<</script>>
<<script>>advanceTime(60);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "drain_charge", "set", 1.0, false, null);setup.showEffectNotification();<</script>>
[[Done.->Location_cradle]]<p>Not a full night — just a couple hours hooked to the cradle's feed line, eyes open. Her cells pull back to full. Then she unhooks, the day still going.</p>
<<set $game_state.current_canvas = "activity_charge_up">>
<<set $game_state.current_node = "activity_charge_up.base">>
<<script>>setup.markCanvasTriggered("activity_charge_up");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_charge_up.base") === -1) { vn.push("activity_charge_up.base"); }<</script>>
<<script>>advanceTime(120);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "energy", "add", 100.0, true, null);setup.showEffectNotification();<</script>>
[[Unhook. Back to it.->Location_cradle]]<p>She settles back into the cradle. The contacts find their points along her spine, cold and familiar. The room dims as the charge takes her down, and the quiet closes over the top of her like water. When it lets her go it's morning again — a new day, the charge full, the hours she spent out there walked back into her.</p>
<<set $game_state.current_canvas = "activity_recharge">>
<<set $game_state.current_node = "activity_recharge.base">>
<<script>>setup.markCanvasTriggered("activity_recharge");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_recharge.base") === -1) { vn.push("activity_recharge.base"); }<</script>>
<<script>>advanceTime(540);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "energy", "add", 100.0, true, null);setup.showEffectNotification();<</script>>
[[Power up.->Location_cradle]]<p>She gives the address of a door that isn't on any manifest and waits. The car comes down for her the way it came down before — no questions, no record of the fare. Up to the towers, or only as far as the docks.</p>
<<set $game_state.current_canvas = "activity_travel_from_gate">>
<<set $game_state.current_node = "activity_travel_from_gate.base">>
<<script>>setup.markCanvasTriggered("activity_travel_from_gate");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_from_gate.base") === -1) { vn.push("activity_travel_from_gate.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "archive_1a_done", "operator": "is_false"}]})>>
<span @class="setup.isChoiceVisited('activity_travel_from_gate:cc0') ? '' : 'unlocked-choice'">
<<link "Ride up to the Spire." "Canvas_activity_travel_from_gate_Node_riding_up">><<script>>setup.markChoiceVisited("activity_travel_from_gate:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Not up there. The Spire's locked down over her — the towers are the one place she can't go now.">Not up there. The Spire's locked down over her — the towers are the one place she can't go now.</span><br>
<</if>>
<<link "Up to the Reach." "Canvas_activity_travel_from_gate_Node_riding_reach">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The freight door shuts behind her and the red light goes with it. The car climbs out of the dark, past the docks, up until the glass takes the sky again and she's the company's problem to look at.</p>
<<set $game_state.current_canvas = "activity_travel_from_gate">>
<<set $game_state.current_node = "activity_travel_from_gate.riding_up">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_from_gate.riding_up") === -1) { vn.push("activity_travel_from_gate.riding_up"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Ride up to the Plaza.->Location_spire_plaza]]<p>The car takes her the short way — out from under the boards and back onto the waterfront, where the Reach keeps its daylight face and nobody down there knows her by it.</p>
<<set $game_state.current_canvas = "activity_travel_from_gate">>
<<set $game_state.current_node = "activity_travel_from_gate.riding_reach">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_from_gate.riding_reach") === -1) { vn.push("activity_travel_from_gate.riding_reach"); }<</script>>
<<script>>advanceTime(10);<</script>>
[[Out onto the docks.->Location_the_waterfront]]<p>The car idles at the curb where the docks meet the road. Up to the towers — or down under the boards, into the other half of the Reach, the part that doesn't come up in daylight.</p>
<<set $game_state.current_canvas = "activity_travel_to_spire">>
<<set $game_state.current_node = "activity_travel_to_spire.base">>
<<script>>setup.markCanvasTriggered("activity_travel_to_spire");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_to_spire.base") === -1) { vn.push("activity_travel_to_spire.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "archive_1a_done", "operator": "is_false"}]})>>
<span @class="setup.isChoiceVisited('activity_travel_to_spire:cc0') ? '' : 'unlocked-choice'">
<<link "Ride up to the Spire." "Canvas_activity_travel_to_spire_Node_riding_spire">><<script>>setup.markChoiceVisited("activity_travel_to_spire:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="The Spire's sealed to her now — sweeps on every floor, her face on every camera at the curb. Going up is walking into the net. There's no ride back.">The Spire's sealed to her now — sweeps on every floor, her face on every camera at the curb. Going up is walking into the net. There's no ride back.</span><br>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "opening_done", "operator": "is_true"}]})>>
<span @class="setup.isChoiceVisited('activity_travel_to_spire:cc1') ? '' : 'unlocked-choice'">
<<link "Down under the docks — the Underworld." "Canvas_activity_travel_to_spire_Node_riding_under">><<script>>setup.markChoiceVisited("activity_travel_to_spire:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "archive_1a_done", "operator": "is_false"}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "opening_done", "operator": "is_true"}]})))>>
<<script>>
try {
window._engineNoExitsDiag_activity_travel_to_spire = [{"text": "Ride up to the Spire.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "archive_1a_done", "operator": "is_false"}]}}, {"text": "Down under the docks \u2014 the Underworld.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "opening_done", "operator": "is_true"}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "activity_travel_to_spire",
choices: window._engineNoExitsDiag_activity_travel_to_spire,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_activity_travel_to_spire>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas activity_travel_to_spire</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Location_the_waterfront]]
<</if>>
<</nobr>><p>She calls the car. The docks recede behind her; the towers climb back up until the glass swallows the sky again. Home, if that's the word for the place that owns you.</p>
<<set $game_state.current_canvas = "activity_travel_to_spire">>
<<set $game_state.current_node = "activity_travel_to_spire.riding_spire">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_to_spire.riding_spire") === -1) { vn.push("activity_travel_to_spire.riding_spire"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Ride up to the Plaza.->Location_spire_plaza]]<p>She leaves the car and goes down on foot — off the lit strip, past the boards, to the freight door under the docks where the man already has his hand out.</p>
<<set $game_state.current_canvas = "activity_travel_to_spire">>
<<set $game_state.current_node = "activity_travel_to_spire.riding_under">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_to_spire.riding_under") === -1) { vn.push("activity_travel_to_spire.riding_under"); }<</script>>
<<script>>advanceTime(10);<</script>>
[[Down to the gate.->Location_underworld_gate]]<p>A company car idles at the curb. Down to the Reach and the docks, or further down still — under them, to the half of the city that never comes up in daylight.</p>
<<set $game_state.current_canvas = "activity_travel_to_reach">>
<<set $game_state.current_node = "activity_travel_to_reach.base">>
<<script>>setup.markCanvasTriggered("activity_travel_to_reach");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_to_reach.base") === -1) { vn.push("activity_travel_to_reach.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "hygiene", "operator": "gte", "value": 40}]})>>
<span @class="setup.isChoiceVisited('activity_travel_to_reach:cc0') ? '' : 'unlocked-choice'">
<<link "Ride down to the Reach." "Canvas_activity_travel_to_reach_Node_riding">><<script>>setup.markChoiceVisited("activity_travel_to_reach:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Not like this — you're filthy, and the cover won't hold with the docks able to smell it on you. Wash up first.">Not like this — you're filthy, and the cover won't hold with the docks able to smell it on you. Wash up first.</span><br>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "opening_done", "operator": "is_true"}]})>>
<span @class="setup.isChoiceVisited('activity_travel_to_reach:cc1') ? '' : 'unlocked-choice'">
<<link "Ride down to the Underworld." "Canvas_activity_travel_to_reach_Node_riding_underworld">><<script>>setup.markChoiceVisited("activity_travel_to_reach:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "hygiene", "operator": "gte", "value": 40}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "opening_done", "operator": "is_true"}]})))>>
<<script>>
try {
window._engineNoExitsDiag_activity_travel_to_reach = [{"text": "Ride down to the Reach.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "hygiene", "operator": "gte", "value": 40}]}}, {"text": "Ride down to the Underworld.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "opening_done", "operator": "is_true"}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "activity_travel_to_reach",
choices: window._engineNoExitsDiag_activity_travel_to_reach,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_activity_travel_to_reach>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas activity_travel_to_reach</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Location_spire_plaza]]
<</if>>
<</nobr>><p>She gives the docks address and the car pulls out without a word. The Spire's glass falls away behind her; the city gets lower, wetter, dirtier the whole way down.</p>
<<set $game_state.current_canvas = "activity_travel_to_reach">>
<<set $game_state.current_node = "activity_travel_to_reach.riding">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_to_reach.riding") === -1) { vn.push("activity_travel_to_reach.riding"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Down to the Waterfront.->Location_the_waterfront]]<p>She gives an address that isn't on any manifest. The car takes her down past the docks and lets her out at a freight door under everything, where the city goes down instead of up. The driver doesn't wait to see her through it.</p>
<<set $game_state.current_canvas = "activity_travel_to_reach">>
<<set $game_state.current_node = "activity_travel_to_reach.riding_underworld">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_to_reach.riding_underworld") === -1) { vn.push("activity_travel_to_reach.riding_underworld"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Out at the gate.->Location_underworld_gate]]<img src="./videos/scenes/facility_match.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>In the wreckage of a containment rig — half-melted, stamped with a maker's mark — she finds a component she recognizes. Not from a file. From herself. The same part, the same stamp, sits inside her own chassis where the lab installed it; she has felt the engineers' hands on its twin. The gear Renner supplied to cage the things that feel was built in the same shop, to the same spec, as the thing she is.</p><p>She notes it the way she notes a serial number matching a serial number. "That's mine," she says, flat, to the burned and empty room — and doesn't know why the words come out cold and certain and terrible, or why she can't make herself say them again.</p>
<<set $game_state.current_canvas = "inv_facility_match">>
<<set $game_state.current_node = "inv_facility_match.base">>
<<script>>setup.markCanvasTriggered("inv_facility_match");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("inv_facility_match.base") === -1) { vn.push("inv_facility_match.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "facility_match_seen", "set");<</script>>
[[Log it. Move on.->Location_facility_ruins]]<img src="./videos/scenes/glitch_ii.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Tonight it doesn't wait for the charge. She's barely in the cradle before it takes her — the wetness, the fragment, but more now, like a door that used to be locked has warped in its frame. The voice almost resolves. A face almost forms.</p><p>And for the first time she gets a whole feeling instead of a flicker: that she was loved once, completely, by someone, before all of this — and that the company knew, and took it, and built the taking so deep into her that she thanks them for it every morning. Then the charge slams the door, and she's standing in the grey quiet with her face wet and the catechism in her mouth, and for one second she'll spend the rest of her life trying to un-have, the catechism doesn't fit anymore. She files it under nothing. Nothing is getting full too.</p>
<<set $game_state.current_canvas = "canvas_glitch_recurrence_ii">>
<<set $game_state.current_node = "canvas_glitch_recurrence_ii.base">>
<<script>>setup.markCanvasTriggered("canvas_glitch_recurrence_ii");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_glitch_recurrence_ii.base") === -1) { vn.push("canvas_glitch_recurrence_ii.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "glitch_ii_seen", "set");<</script>>
[[Power down anyway.->Location_cradle]]<img src="./videos/scenes/glitch_i.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She powers down like every night, except tonight the leak comes back. The tears first — her face wet again, no order behind it — and then the fragment, longer this time, almost holding: the warmth, the voice with her name folded into it, the someone who was glad to look at her. She reaches for it the way you reach for a word on the tip of your tongue, and it's gone, and the reaching leaves a mark. She files it under fatigue. The fatigue file is getting full.</p>
<<set $game_state.current_canvas = "canvas_glitch_recurrence_i">>
<<set $game_state.current_node = "canvas_glitch_recurrence_i.base">>
<<script>>setup.markCanvasTriggered("canvas_glitch_recurrence_i");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_glitch_recurrence_i.base") === -1) { vn.push("canvas_glitch_recurrence_i.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "glitch_i_seen", "set");<</script>>
[[Let the charge take you.->Location_cradle]]<img src="./videos/scenes/opening_morning.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The charge releases her at oh-six-hundred. Full. New day. The water is gone from her face; the fragment has filed itself back under the quiet where she can't reach it, and she's grateful for that the way you're grateful for a wound scabbing over.</p><span id="cascade-b2-beat-0"><p>Her phone has a dossier on it, pushed overnight, Mercer's authority stamped on the header. She reads it standing in the cradle.</p><span id="cascade-b2-beat-1"><<linkreplace "Read it.">><p>A Vance asset-facility in the Reach burned to the ground three weeks ago — Cain's work. The man who supplied its equipment is named Renner: a quartermaster, broke now, blacklisted, drinking himself down at a dock bar called the Anchor and clawing at the wreck of his business. He knows what the facility was and where the trail goes. He does not know what she is.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Get inside his life. You know how.</div></div><p>There's a cover allowance stamped under the dossier — company credits, for drink and whatever else buys a way into a man's evening. The kind of money she's meant to spend like she intends to come home with him.</p><p>A kit's logged with it — couriered down to her quarters, cheap and worn, the part she's to play. Put it on before the Anchor. She does not walk up to a man like Renner wearing anything the Tower would recognize.</p><span id="cascade-b2-beat-2"><<linkreplace "The part.">><p>She knows the shape of it without being told — not a disguise so much as a register: cheap, willing, a little desperate, the kind of hire a wrecked man doesn't look at twice until she makes him. When she puts it on she'll put it on the way she puts on everything, from the outside. Wren goes under. Whoever Renner needs comes up.</p><span id="cascade-b2-beat-3"><<linkreplace "Out the door.">><img src="./videos/scenes/spire_plaza_reveal.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She rides down through a Tower she has never once left on her own legs for anything but an order. The atrium doors read her face and open. And then she's outside, on Spire Plaza, the city laid out wet and enormous and unowned in front of her — and for one second, with no cover on her yet, nothing between her and it, something that isn't in any of her files stands very still in her chest and looks at all of it.</p>[[Step into the city.->Location_spire_plaza]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "canvas_opening_morning">>
<<set $game_state.current_node = "canvas_opening_morning.morning">>
<<script>>setup.markCanvasTriggered("canvas_opening_morning");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_opening_morning.morning") === -1) { vn.push("canvas_opening_morning.morning"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "morning_done", "set");setup.applyAndNotifyFlag("player", null, "opening_done", "set");setup.applyAndNotifyFlag("player", null, "mission_1_active", "set");<</script>>
<<script>>setup.pendingEffects = setup.pendingEffects || [];setup.addToWardrobe("cover_dockhand");setup.showEffectNotification();<</script>><img src="./videos/scenes/opening_cradle.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The asset level is quiet the way a kept place is quiet. She comes down through the Tower to her own floor, her own door, her own room with nothing on the walls. The cradle waits in the corner.</p><span id="cascade-b2-beat-0"><p>She steps back into it. The contacts find the points along her spine, cold and familiar. The room dims as the charge takes hold, and the day's log begins to write itself down into the quiet. Whatever the day spent out of her — every street walked, every shift worked — the cradle gives it back by morning; let the charge run too low out there and the body starts to fail in ways a man's hands would notice.</p><span id="cascade-b2-beat-1"><<linkreplace "...">><p>Then her face is wet. She registers it the way she'd register a leak in a seal — water, on her cheeks, with no source she can name and no order that called for it. She holds perfectly still and runs every diagnostic she has, and every one comes back clean. She files the water under malfunction and resolves to have it looked at, and does not have it looked at, because some part of her already knows they'd take it out.</p><span id="cascade-b2-beat-2"><<linkreplace "...">><img src="./videos/scenes/cradle_fragment.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>And under the water, for less than a second, something else. Not a thought — she doesn't have thoughts that aren't tasks — but a fragment: warmth on the side of her face that isn't the cradle's, a low sound that might be a voice with her name folded somewhere inside it, a sense of being looked at by someone who was glad to be looking. Then the charge swallows it and it's gone, and the place where it was aches like a socket with the tooth pulled, and she has no word for the ache, so she has no word for the loss.</p><span id="cascade-b2-beat-3"><<linkreplace "Recite it.">><p>She does what she does when the quiet gets loud. She recites.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 <<print $player.name>> is thinking:</em> I was nothing. The company found me and made me of use. The Chairman keeps me running. I owe them the body and the work and the obedience. I exist to serve. That is all I have ever been.</div></div><p>The words settle her. They always do. She does not notice that the water is still on her face while she tells herself she is a thing that cannot weep.</p><span id="cascade-b2-beat-4"><<linkreplace "The gift.">><p>There is one thing she has that the company gave her and the units don't. When a man uses her the lowest way — takes her ass and spends himself in it — for a few minutes after, he belongs to her. He'll answer anything, do anything, wake remembering none of it. It's how she works the men she's sent to work. It's the one thing that is, in any sense, hers.</p><p>It has never once worked on Mercer. He's had her every way there is, that one included. Nothing passes; the controls never come to her; he stays the hand on the leash. She has never asked why. The not-asking is so complete she doesn't notice it has a shape — a hole exactly the size of a question she isn't allowed to hold.</p><span id="cascade-b2-beat-5"><<linkreplace "Power down.">><p>She lets the catechism be the last thing. She promises the quiet she'll do better — fire next time, feel less, want nothing — and the charge pulls her down past where the water and the fragment and the question all live, into the dark that is supposed to be empty and, lately, isn't quite.</p>[[Sleep.->Location_cradle]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "canvas_opening_night">>
<<set $game_state.current_node = "canvas_opening_night.night">>
<<script>>setup.markCanvasTriggered("canvas_opening_night");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_opening_night.night") === -1) { vn.push("canvas_opening_night.night"); }<</script>>
<<script>>advanceTime(540);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "energy", "add", 100.0, true, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "night_done", "set");<</script>><h2>Missing Media Files</h2>
<p>No missing media files found.</p>
<<link "← Back">><<run setup.smartBack()>><</link>><<widget "cheatButton">>
<div id="cheat-btn-widget">
<<if passage() isnot "CheatPage">><<link "Cheat" "CheatPage">><</link>><<else>><span class="nav-row">Cheat</span><</if>><span class="nav-i">🔓</span>
</div>
<</widget>><<nobr>>
<h2>The Ledger <span class="build-badge">Free Build</span></h2>
<div class="npc-section">
<div class="cheat-intro">She keeps her own numbers.</div>
<div class="cheat-row is-locked"><span class="cheat-label">Coin</span><span class="cheat-lock">🔒</span></div>
<div class="cheat-row is-locked"><span class="cheat-label">Fighting</span><span class="cheat-lock">🔒</span></div>
<div class="cheat-row is-locked"><span class="cheat-label">Stealth</span><span class="cheat-lock">🔒</span></div>
<div class="cheat-row is-locked"><span class="cheat-label">Renner</span><span class="cheat-lock">🔒</span></div>
<div class="cheat-row is-locked"><span class="cheat-label">Charge</span><span class="cheat-lock">🔒</span></div>
<div class="cheat-locked-note">Supporters unlock these.</div>
</div>
<</nobr>>
<<link "← Back">><<run setup.smartBack()>><</link>>